From 35798124e785ca14ea8a6a33075fd7a63101c7bf Mon Sep 17 00:00:00 2001 From: li kesong Date: Sat, 23 Apr 2022 10:05:51 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=BA=E4=B8=B0=E5=90=8C=E5=9F=8E=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MySQL_change.sql | 18 ++ app/Controller/v3/OrderOnlineController.php | 39 ++- app/Controller/v3/SfExpressController.php | 223 +++++++++++++++ app/Controller/v3/UserAddressController.php | 8 +- app/Model/v3/Goods.php | 22 ++ app/Model/v3/GoodsActivity.php | 22 ++ app/Model/v3/SfExpressOrder.php | 14 + .../v3/Implementations/OrderOnlineService.php | 108 +++++++- .../v3/Implementations/UserAddressService.php | 63 ++++- .../UserAddressServiceInterface.php | 2 +- app/Service/v3/SfExpress.php | 262 ++++++++++++++++++ config/routes.php | 11 + 小程序修改 | 3 + 13 files changed, 766 insertions(+), 29 deletions(-) create mode 100644 MySQL_change.sql create mode 100644 app/Controller/v3/SfExpressController.php create mode 100644 app/Model/v3/SfExpressOrder.php create mode 100644 app/Service/v3/SfExpress.php create mode 100644 小程序修改 diff --git a/MySQL_change.sql b/MySQL_change.sql new file mode 100644 index 0000000..ea494a6 --- /dev/null +++ b/MySQL_change.sql @@ -0,0 +1,18 @@ +# 2022-04-09 14:57 +ALTER TABLE `lanzu_order_main` + CHANGE COLUMN `shipping_type` `shipping_type` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '配送方式:1服务站配送,2达达配送,3自提,4顺丰配送' AFTER `global_order_id`, + CHANGE COLUMN `delivery_time_note` `delivery_time_note` VARCHAR(100) NULL DEFAULT '' COMMENT '客户期望送达时间' COLLATE 'utf8mb4_general_ci' AFTER `shipping_name`; + +# 2022-04-11 23:50 +ALTER TABLE `lanzu_goods` + ADD COLUMN `weight` INT(11) NOT NULL DEFAULT '0' COMMENT '产品毛重,用于计算顺丰运费' AFTER `goods_unit`; +ALTER TABLE `lanzu_goods_activity` + ADD COLUMN `weight` INT(11) NOT NULL DEFAULT '0' COMMENT '产品毛重,用于计算顺丰运费' AFTER `goods_unit`; + +# 2022-04-17 23:41 +ALTER TABLE `lanzu_order_goods` + ADD COLUMN `weight` INT NULL DEFAULT 0 COMMENT '一件商品的重量(多个需要*number),单位:克' AFTER `note`; + + +# 顺丰订单记录 +create table `lanzu_sf_express_orders`; \ No newline at end of file diff --git a/app/Controller/v3/OrderOnlineController.php b/app/Controller/v3/OrderOnlineController.php index 1fcd8c8..f20669e 100644 --- a/app/Controller/v3/OrderOnlineController.php +++ b/app/Controller/v3/OrderOnlineController.php @@ -4,23 +4,22 @@ namespace App\Controller\v3; use App\Constants\v3\ErrorCode; use App\Constants\v3\LogLabel; -use App\Constants\v3\OrderState; use App\Controller\BaseController; use App\Exception\ErrorCodeException; use App\Model\v3\Market; -use App\Model\v3\OrderMain; use App\Request\v3\OrderOnlineDetailRequest; use App\Request\v3\OrderOnlineRequest; use App\Request\v3\OrderOnlineStateRequest; use App\Request\v3\UserRequest; -use App\Service\v3\Implementations\PaymentService; use App\Service\v3\Interfaces\CouponRecServiceInterface; use App\Service\v3\Interfaces\DistributionPriceServiceInterface; use App\Service\v3\Interfaces\LocationServiceInterface; use App\Service\v3\Interfaces\SeparateAccountsServiceInterface; use App\Service\v3\Interfaces\ShopCartServiceInterface; +use App\Service\v3\SfExpress; use GuzzleHttp\Client; use Hyperf\DbConnection\Db; +/** @var Inject 注解使用 */ use Hyperf\Di\Annotation\Inject; use App\Service\v3\Interfaces\OrderOnlineServiceInterface; use App\Service\v3\Interfaces\UserBindTelServiceInterface; @@ -127,6 +126,7 @@ class OrderOnlineController extends BaseController $collection = collect($addressNew); $sorted = $collection->sortBy('distance')->sortByDesc('is_default'); $address = $sorted->values()->first(); + $res['store_list'] = $this->shopCartService->getGoodsByShopcartId($shopcartIds); if($address){ $distance = $address['distance']; if($distance >= 1000){ @@ -139,9 +139,34 @@ class OrderOnlineController extends BaseController }else{ $address['tags'][0] = ['id' => 6,'name' => '距离最近']; } - $distributionPrice = $this->distributionPriceService->do($distance); - $originalPrice = $this->distributionPriceService->original($distance); - $res['location'] = [ + + try { + $weight = 0; + foreach ($res['store_list'] as $item) { + foreach ($item['shopping_cart'] as $v) { + $weight += $v['goods']['weight'] * $v['num']; + } + } + # 因首次进入时是尽快送达,故此处不用处理预约单 + $distributionPrice = SfExpress::getInstance()->getDeliveryCost([ + 'user_lng' => $address['lng'], + 'user_lat' => $address['lat'], + 'user_address' => $address['address'], + 'weight' => $weight, + 'shop' => [ + 'shop_name' => $market->name, + 'shop_phone' => $market->tel, + 'shop_address' => $market->address, + 'shop_lng' => $market->lng, + 'shop_lat' => $market->lat, + ], + ]); + $originalPrice = SfExpress::getInstance()->getOriginDeliveryCost($distributionPrice); + } catch (\Exception $exception) { + return $this->result(500, [], $exception->getMessage()); + } + + $res['location'] = [ 'address' => $address, 'distribution_price' => $distributionPrice, 'original_price' => $originalPrice, @@ -169,8 +194,6 @@ class OrderOnlineController extends BaseController //返回预约送达时间 数组 $res['appointment_time'] = $this->appointmentTimeService->get($shopcartIds); - - $res['store_list'] = $this->shopCartService->getGoodsByShopcartId($shopcartIds); //获取用户优惠券 $coupons = $this->couponRecService->allForOnlineOrderAvailable( $userId, $marketId, explode(',', $shopcartIds) ); $res['coupon'] = [ diff --git a/app/Controller/v3/SfExpressController.php b/app/Controller/v3/SfExpressController.php new file mode 100644 index 0000000..255c1d7 --- /dev/null +++ b/app/Controller/v3/SfExpressController.php @@ -0,0 +1,223 @@ +request和$this->response获取不到 + $this->dev_id = env('SF_EXPRESS_DEV_ID'); + $this->dev_key = env('SF_EXPRESS_DEV_KEY'); +// $this->shop_id = env('SF_EXPRESS_SHOP_ID'); + } + + /** + * 配送状态更改回调 + */ + public function riderStatus(): ResponseInterface + { + if (!$this->checkSign($this->request->query('sign'))) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '签名错误', + ]); + } + + $formData = $this->request->all(); + if (empty($formData['shop_order_id'])) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '无效的shop_order_id', + ]); + } + + # 主订单表 + $orderMain = OrderMain::whereIn('state', [OrderState::PAID, OrderState::DELIVERY]) + ->where('global_order_id', $formData['shop_order_id']) + ->first(); + + if (!$orderMain) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '订单不存在', + ]); + } + + DB::beginTransaction(); + try { + # 主订单表 + $orderMain->state = OrderState::DELIVERY; + //order_status 订单状态 10-配送员确认;12:配送员到店;15:配送员配送中 + if (!empty($formData['order_status'])) { + if ($formData['order_status'] == 10 && $orderMain->receive_time == 0) { + $orderMain->receive_time = time(); // 接单时间 + } else if ($formData['order_status'] == 12 && $orderMain->delivery_start_time == 0) { + $orderMain->delivery_start_time = time(); // 开始配送时间 + } + } + $orderMain->save(); + + # 子订单表 + Order::where('order_main_id', $formData['shop_order_id']) + ->update(['state' => $orderMain->state]); + + DB::commit(); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => $exception->getMessage(), + ]); + } + + return $this->response->json([ + 'error_code' => 0, + 'error_msg' => 'success', + ]); + } + + /** + * 骑士撤单状态回调 + */ + public function riderRecall(): ResponseInterface + { + if (!$this->checkSign($this->request->query('sign'))) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '签名错误', + ]); + } + + $formData = $this->request->all(); + if (empty($formData['shop_order_id'])) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '无效的shop_order_id', + ]); + } + + //order_status 22-配送员撤单 + //TODO 因目前订单表没有相关撤单的字段,故目前不做处理,仅仅返回成功给顺丰 + return $this->response->json([ + 'error_code' => 0, + 'error_msg' => 'success', + ]); + } + + /** + * 订单完成回调 + */ + public function orderComplete(): ResponseInterface + { + if (!$this->checkSign($this->request->query('sign'))) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '签名错误', + ]); + } + + $formData = $this->request->all(); + if (empty($formData['shop_order_id'])) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '无效的shop_order_id', + ]); + } + + # 主订单表 + $orderMain = OrderMain::where('state', '>', OrderState::UNPAID) + ->where('global_order_id', $formData['shop_order_id']) + ->first(); + + if (!$orderMain) { + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => '订单不存在', + ]); + } + + DB::beginTransaction(); + try { + # 主订单表 + $orderMain->state = OrderState::COMPLETED; + $orderMain->complete_time = time(); + $orderMain->delivery_time = time(); + $orderMain->save(); + + # 子订单表 + Order::where('order_main_id', $formData['shop_order_id']) + ->update(['state' => $orderMain->state]); + + DB::commit(); + } catch (\Exception $exception) { + DB::rollBack(); + return $this->response->json([ + 'error_code' => 500, + 'error_msg' => $exception->getMessage(), + ]); + } + + return $this->response->json([ + 'error_code' => 0, + 'error_msg' => 'success', + ]); + } + + /** + * 顺丰原因订单取消回调 + */ + public function sfCancel(): ResponseInterface + { + //TODO 因目前订单表没有相关撤单的字段,故目前不做处理,仅仅返回成功给顺丰 + return $this->riderRecall(); + } + + /** + * 订单异常回调 + */ + public function riderException(): ResponseInterface + { + /** ex_id枚举值: + 4003:托寄物丢失或损坏 + 1001:商家出货慢 + 2010:顾客拒绝实名认证 + 3004:实名认证校验失败 + 1007:更改取货地址 + 2001:顾客电话无法接通 + 2004:更改期望送达时间 + 2005:顾客拒收 + 2008:顾客不在家 + 2009:更改送货地址 + 4001:配送地址错误 + 4002:其他 + */ + //TODO 因目前订单表没有相关撤单的字段,故目前不做处理,仅仅返回成功给顺丰 + return $this->riderRecall(); + } + + /** + * 回调签名校验 + */ + private function checkSign(?string $checkSign): bool + { + $post_data = $this->request->getBody()->getContents(); + echo PHP_EOL, '$post_data', PHP_EOL; + print_r($post_data); + $sign_char = $post_data . "&{$this->dev_id}&{$this->dev_key}"; + return $checkSign == base64_encode(md5($sign_char)); + } +} \ No newline at end of file diff --git a/app/Controller/v3/UserAddressController.php b/app/Controller/v3/UserAddressController.php index 985dcc2..f32969d 100644 --- a/app/Controller/v3/UserAddressController.php +++ b/app/Controller/v3/UserAddressController.php @@ -8,7 +8,9 @@ use App\Request\v3\UserAddressRequest; use App\Request\v3\UserAddressUpdateRequest; use App\Request\v3\UserRequest; use App\Service\v3\Interfaces\UserAddressServiceInterface; +/** @var Inject 注解使用 */ use Hyperf\Di\Annotation\Inject; + class UserAddressController extends BaseController { /** @@ -66,8 +68,10 @@ class UserAddressController extends BaseController { $userAddressId = $this->request->input('user_address_id'); $marketId = $this->request->input('market_id'); - $res = $this->userAddressService->getAddressAndDistributionPrice($userAddressId,$marketId); - return $this->success(['location' => $res]); + $shopCartIds = $this->request->input('shopcart_ids'); + $deliveryTimeNote = $this->request->input('delivery_time_note'); + $res = $this->userAddressService->getAddressAndDistributionPrice($userAddressId, $marketId, $shopCartIds, $deliveryTimeNote); + return $this->success(['location' => $res]); } public function deliveryDistance(){ diff --git a/app/Model/v3/Goods.php b/app/Model/v3/Goods.php index 892904c..6ca319e 100644 --- a/app/Model/v3/Goods.php +++ b/app/Model/v3/Goods.php @@ -189,4 +189,26 @@ class Goods extends Model { return $this->hasMany(GoodsBanner::class, 'goods_id','id'); } + + /** + * add:2022-04-11,获取产品毛重,主要用于计算顺丰运费 + */ + public function getWeightAttribute($value): int + { + if (empty($value) && !empty($this->attributes['goods_unit'])) { + $goods_unit = $this->attributes['goods_unit']; + switch (true) { + case preg_match('/(\d{2,})\D*±(\d{2,})/', $goods_unit, $matches): + array_shift($matches); + $value = (int)max($matches); + break; + case preg_match_all('/(\d{2,})[g|克]/', $goods_unit, $matches): + $value = (int)max($matches[1]); + break; + default: + $value = 0; + } + } + return $value ?: 300; // 如果获取不到重量,默认为300g + } } \ No newline at end of file diff --git a/app/Model/v3/GoodsActivity.php b/app/Model/v3/GoodsActivity.php index dfaa39f..7329065 100644 --- a/app/Model/v3/GoodsActivity.php +++ b/app/Model/v3/GoodsActivity.php @@ -143,4 +143,26 @@ class GoodsActivity extends Model return $this->attachmentService->switchImgToAliOss($item); }); } + + /** + * add:2022-04-11,获取产品毛重,主要用于计算顺丰运费 + */ + public function getWeightAttribute($value): int + { + if (empty($value) && !empty($this->attributes['goods_unit'])) { + $goods_unit = $this->attributes['goods_unit']; + switch (true) { + case preg_match('/(\d{2,})\D*±(\d{2,})/', $goods_unit, $matches): + array_shift($matches); + $value = (int)max($matches); + break; + case preg_match_all('/(\d{2,})[g|克]/', $goods_unit, $matches): + $value = (int)max($matches[1]); + break; + default: + $value = 0; + } + } + return $value ?: 300; // 如果获取不到重量,默认为300g + } } \ No newline at end of file diff --git a/app/Model/v3/SfExpressOrder.php b/app/Model/v3/SfExpressOrder.php new file mode 100644 index 0000000..ed2dce6 --- /dev/null +++ b/app/Model/v3/SfExpressOrder.php @@ -0,0 +1,14 @@ +belongsTo(OrderMain::class, 'shop_order_id', 'global_order_id'); + } +} \ No newline at end of file diff --git a/app/Service/v3/Implementations/OrderOnlineService.php b/app/Service/v3/Implementations/OrderOnlineService.php index d6f6683..25dbb70 100644 --- a/app/Service/v3/Implementations/OrderOnlineService.php +++ b/app/Service/v3/Implementations/OrderOnlineService.php @@ -3,7 +3,6 @@ namespace App\Service\v3\Implementations; use App\Commons\Log; -use App\Constants\v3\Employee; use App\Constants\v3\ErrorCode; use App\Constants\v3\LogLabel; use App\Constants\v3\OrderState; @@ -16,11 +15,11 @@ use App\Model\v3\Coupon; use App\Model\v3\Employees; use App\Model\v3\Goods; use App\Model\v3\GoodsActivity; -use App\Model\v3\Market; use App\Model\v3\Order; use App\Model\v3\OrderGoods; use App\Model\v3\OrderMain; use App\Model\v3\OrderSalesStatistic; +use App\Model\v3\SfExpressOrder; use App\Model\v3\ShoppingCart; use App\Model\v3\Store; use App\Model\v3\User; @@ -36,10 +35,12 @@ use App\Service\v3\Interfaces\MiniprogramServiceInterface; use App\Service\v3\Interfaces\PaymentServiceInterface; use App\Service\v3\Interfaces\ShopCartUpdateServiceInterface; use App\Service\v3\Interfaces\UserAddressServiceInterface; +use App\Service\v3\SfExpress; use App\TaskWorker\SSDBTask; use Exception; use Hyperf\Database\Model\Model; use Hyperf\DbConnection\Db; +/** @var Inject 注解使用 */ use Hyperf\Di\Annotation\Inject; use App\Service\v3\Interfaces\OrderOnlineServiceInterface; use Hyperf\Redis\Redis; @@ -136,8 +137,7 @@ class OrderOnlineService implements OrderOnlineServiceInterface * @return array[] */ public function do($marketId, $userId, $userAddrId, $storeList, $totalMoney, $deliveryTimeNote='尽快送达', $serviceMoney=0, $receiveCouponIds=null, $plat='', $selfTake=0){ - - $redis = ApplicationContext::getContainer()->get(Redis::class); + // $redis = ApplicationContext::getContainer()->get(Redis::class); $isCacheInventory = false; $carts = []; Db::beginTransaction(); @@ -162,7 +162,8 @@ class OrderOnlineService implements OrderOnlineServiceInterface if ($selfTake != 1) { // 用户收货地址 // 获取配送费用 - $userAddrAndDPrice = $this->userAddressService->getAddressAndDistributionPrice($userAddrId, $marketId); + $shopCartIds = join(',', array_column($storeList, 'cart_ids')); + $userAddrAndDPrice = $this->userAddressService->getAddressAndDistributionPrice($userAddrId, $marketId, $shopCartIds, $deliveryTimeNote); $userAddr = $userAddrAndDPrice['address']['address']; $deliveryAmount = $userAddrAndDPrice['distribution_price']; $deliveryDistance = $userAddrAndDPrice['delivery_distance']; @@ -311,6 +312,7 @@ class OrderOnlineService implements OrderOnlineServiceInterface 'goods_unit' => $goods->goods_unit ?: '', 'cover_img' => $goods->cover_img ?: '', 'spec' => json_encode($goods->spec), + 'weight' => $goods->weight ?: 0, // add:2022-04-17 增加商品重量 ]; } @@ -651,6 +653,9 @@ class OrderOnlineService implements OrderOnlineServiceInterface Db::commit(); + # 创建顺丰订单 + $this->createSfExpressOrder($globalOrderId); + // 释redis库存 $redis = ApplicationContext::getContainer()->get(Redis::class); foreach ($orderGoods as $k => &$goodsItem) { @@ -697,6 +702,99 @@ class OrderOnlineService implements OrderOnlineServiceInterface } } + /** + * 支付回调之后,协程创建顺丰订单 + */ + private function createSfExpressOrder($globalOrderId) + { + co(function () use ($globalOrderId) { + try { + # 主订单 + $orderMain = OrderMain::where('global_order_id', $globalOrderId)->first(); + + $weight = 0; + $product_detail = []; + + # 子订单 + $subOrderIds = Order::where('order_main_id', $globalOrderId)->pluck('id')->toArray(); + $orderGoods = OrderGoods::whereIn('order_id', $subOrderIds)->get(); + foreach ($orderGoods as $goods) { + $weight += $goods->number * $goods->weight; + $product_detail[] = ['product_name' => $goods->name, 'product_num' => $goods->number]; + } + + # 创建顺丰订单 + $sfOrder = SfExpress::getInstance()->createOrder([ + 'shop_order_id' => $globalOrderId, + 'order_time' => time(), + 'receive' => [ + 'user_name' => $orderMain->name, //必填,用户姓名 + 'user_phone' => $orderMain->tel, //必填,用户电话 + 'user_address' => $orderMain->address, //必填,用户详细地址 + 'user_lng' => $orderMain->lng, //必填,用户地址经度 + 'user_lat' => $orderMain->lat, //必填,用户地址纬度 + ], + 'order_detail' => [ // 必填,订单详情 + 'total_price' => 0, //用户订单商品总金额(单位:分) 100 表示1元(最大值为100万, 超过此值则按100万计算) + 'product_type' => 6, //必填,物品类型。1:快餐,2:药品,3:百货,4:脏衣服收,5:干净衣服派,6:生鲜,其它详见文档 + // 'user_money' => 0, // 用户实付商家金额(单位:分) 100 表示1元 + // 'shop_money' => 0, // 商家实收用户金额(单位:分) 100 表示1元 + 'weight_gram' => $weight, // 必填,物品重量(单位:克) 100 表示100g + // 'volume_litre' => 0, // 物品体积(单位:升) 1 表示1升 + // 'delivery_money' => 0, // 商家收取用户的配送费(单位:分) 100 表示1元 + 'product_num' => $orderGoods->sum('number'), // 必填,物品个数 + 'product_type_num' => $orderGoods->count(), // 必填,物品种类个数 + 'product_detail' => $product_detail, /*[ // 必填,物品种类个数 + [ + 'product_name' => '', // 必填,物品名称 + // 'product_id' => 1, // 物品ID + 'product_num' => 1, // 必填,物品数量 + // 'product_price' => 0, // 物品价格 + // 'product_unit' => 0, // 物品单位 + // 'product_remark' => 0, // 备注 + // 'item_detail' => 0, // 详情 + ], + ],*/ + ], + ]); + + # 保存到顺丰订单表 + SfExpressOrder::unguard(); + SfExpressOrder::updateOrCreate(['shop_order_id' => $globalOrderId], [ + 'shop_order_id' => $globalOrderId, + 'sf_order_id' => $sfOrder['result']['sf_order_id'], + 'sf_bill_id' => $sfOrder['result']['sf_bill_id'], + 'total_price' => $sfOrder['result']['total_price'] ?? 0, + 'delivery_distance_meter' => $sfOrder['result']['delivery_distance_meter'] ?? 0, + 'weight_gram' => $sfOrder['result']['weight_gram'] ?? 0, + 'start_time' => $sfOrder['result']['start_time'] ?? 0, + 'expect_time' => $sfOrder['result']['expect_time'] ?? 0, + 'total_pay_money' => $sfOrder['result']['total_pay_money'] ?? 0, + 'real_pay_money' => $sfOrder['result']['real_pay_money'] ?? 0, + ]); + echo '创建顺丰订单成功了!', PHP_EOL; + } catch (Exception $exception) { + $this->log->event('SfExpress', ['exception' => $exception->getMessage()]); + + # 取消顺丰订单,如果失败,重试 3 次 + co(function () use ($globalOrderId) { + for ($i = 0; $i < 3; $i++) { + try { + $res = SfExpress::getInstance()->cancelOrder(['order_id' => $globalOrderId]); + if ($res['error_code'] == 0) { + echo "取消顺丰订单 $globalOrderId 成功!", PHP_EOL; + break; + } + } catch (Exception $exception) { + echo $exception->getMessage(), PHP_EOL; + } + sleep(1); + } + }); + } + }); + } + /** * @inheritDoc */ diff --git a/app/Service/v3/Implementations/UserAddressService.php b/app/Service/v3/Implementations/UserAddressService.php index 442cee9..e0b066c 100644 --- a/app/Service/v3/Implementations/UserAddressService.php +++ b/app/Service/v3/Implementations/UserAddressService.php @@ -1,6 +1,5 @@ where('user_id',$userId)->get(); } - /** - * @param $userAddressId - * @param $marketId - * @return false|float - */ - public function getAddressAndDistributionPrice($userAddressId,$marketId) - { + /** + * @param $userAddressId + * @param $marketId + * @param string $shopCartIds + * @param string $deliveryTimeNote + * @return array + */ + public function getAddressAndDistributionPrice($userAddressId, $marketId, string $shopCartIds, string $deliveryTimeNote) + { $address = $this->get($userAddressId); - $market = Market::query()->select('lng','lat')->find($marketId); + $market = Market::find($marketId); if(empty($address['address']->lng) || empty($address['address']->lat) || empty($market->lng) || empty($market->lat)){ throw new ErrorCodeException(ErrorCode::LOCATION_USER_ADDRESS); } $distance = $this->locationService->getDistanceByTencent($market->lng,$market->lat,$address['address']->lng,$address['address']->lat); - $distributionPrice = $this->distributionPriceService->do($distance); - $originalPrice = $this->distributionPriceService->original($distance); - if($distance >= 1000){ + + $storeList = $this->shopCartService->getGoodsByShopcartId($shopCartIds); + $weight = 0; + foreach ($storeList as $item) { + foreach ($item['shopping_cart'] as $v) { + $weight += $v['goods']['weight'] * $v['num']; + } + } + + $sfParams = [ + 'user_lng' => $address['address']['lng'], + 'user_lat' => $address['address']['lat'], + 'user_address' => $address['address']['address'], + 'weight' => $weight, + 'shop' => [ + 'shop_name' => $market->name, + 'shop_phone' => $market->tel, + 'shop_address' => $market->address, + 'shop_lng' => $market->lng, + 'shop_lat' => $market->lat, + ], + ]; + + # 预约单处理 + $sfParams = array_merge($sfParams, SfExpress::getInstance()->deliveryTimeNote2expectTime($deliveryTimeNote)); + + $distributionPrice = SfExpress::getInstance()->getDeliveryCost($sfParams); + $originalPrice = SfExpress::getInstance()->getOriginDeliveryCost($distributionPrice); + + if($distance >= 1000){ $distance_text = '距您收货地址 ' . bcdiv($distance,1000,2) . 'km'; }else{ $distance_text = '距您收货地址 ' . $distance . 'm'; diff --git a/app/Service/v3/Interfaces/UserAddressServiceInterface.php b/app/Service/v3/Interfaces/UserAddressServiceInterface.php index f9b6253..376c6e8 100644 --- a/app/Service/v3/Interfaces/UserAddressServiceInterface.php +++ b/app/Service/v3/Interfaces/UserAddressServiceInterface.php @@ -12,5 +12,5 @@ interface UserAddressServiceInterface public function get($userAddressId); public function getList($userId); public function setDefault($userId,$userAddressId); - public function getAddressAndDistributionPrice($userAddressId,$marketId); + public function getAddressAndDistributionPrice($userAddressId, $marketId, string $shopCartIds, string $deliveryTimeNote); } \ No newline at end of file diff --git a/app/Service/v3/SfExpress.php b/app/Service/v3/SfExpress.php new file mode 100644 index 0000000..d06142d --- /dev/null +++ b/app/Service/v3/SfExpress.php @@ -0,0 +1,262 @@ +dev_id = env('SF_EXPRESS_DEV_ID'); + $this->dev_key = env('SF_EXPRESS_DEV_KEY'); + $this->shop_id = env('SF_EXPRESS_SHOP_ID'); + $this->clientFactory = ApplicationContext::getContainer()->get(ClientFactory::class); + } + + public static function getInstance(): SfExpress + { + if (is_null(self::$_instance)) { + self::$_instance = new static(); + } + return self::$_instance; + } + + /** + * 创建订单 + */ + public function createOrder(array $params): array + { + if (!isset($params['shop_order_id'], $params['order_time'], $params['receive'], $params['order_detail'])) { + throw new BusinessException(500, '商户订单号、用户下单时间、收货信息、订单信息不能为空'); + } + + $params = array_merge([ + 'dev_id' => $this->dev_id, //必填 + 'shop_id' => $this->shop_id, //必填 + 'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID + 'shop_order_id' => '', //商户订单号 + 'shop_preparation_time' => 15, //商家预计备餐时长,分钟级时间 比如: 10 分钟 则传入 10 + 'order_source' => '街链平台', //必填。订单接入来源 1:美团;2:饿了么;3:百度;4:口碑;其他请直接填写中文字符串值 + // 'order_sequence' => '', //取货序号 与order_source配合使用 如:饿了么10号单,表示如下:order_source=2;order_sequence=10。用于骑士快速寻找配送物 + // 'lbs_type' => 2, //坐标类型,1:百度坐标,2:高德坐标 + 'pay_type' => 1, //必填,用户支付方式:1、已支付 0、货到付款 + 'order_time' => time(), + 'is_appoint' => 0, //必填,是否是预约单,0:非预约单;1:预约单 + 'appoint_type' => 0, //预约单类型,1:预约单送达单;2:预约单上门单,默认:0 + // 'expect_time' => '', //用户期望送达时间,若传入自此段且时间大于配送时效,则按照预约送达单处理,时间小于配送时效按照立即单处理;appoint_type=1时需必传,秒级时间戳 + // 'expect_pickup_time' => '', //用户期望上门时间,appoint_type=2时需必传,秒级时间戳 + // 'shop_expect_time' => 0, //商家期望送达时间 只展示给骑士,不参与时效考核;秒级时间戳 + 'is_insured' => 0, //必填,是否保价,0:非保价;1:保价 + 'is_person_direct' => 0, //必填,是否是专人直送订单,0:否;1:是 + // 'vehicle' => 0, //配送交通工具,0:否;1:电动车;2:小轿车 + // 'declared_value' => 0, //保价金额 + // 'gratuity_fee' => 0, //订单小费,不传或者传0为不加小费,单位分,加小费最低不能少于100分 + 'remark' => '无备注', //订单备注 + 'rider_pick_method' => 1, //物流流向,1:从门店取件送至用户;2:从用户取件送至门店 + 'return_flag' => 511, //1:商品总价格,2:配送距离,4:物品重量,8:起送时间,16:期望送达时间,32:支付费用,64:实际支持金额,128:优惠券总金额,256:结算方式,例如全部返回为填入511 + 'push_time' => time(), //必填,推单时间,秒级时间戳(格林尼治时间) + 'version' => 17, //版本号 参照文档主版本号填写 如:文档版本号1.7,version=17 + 'receive' => [ //收货人信息 Obj,详见receive结构 + 'user_name' => '', //必填,用户姓名 + 'user_phone' => '', //必填,用户电话 + 'user_address' => '', //必填,用户详细地址 + 'user_lng' => 0, //必填,用户地址经度 + 'user_lat' => 0, //必填,用户地址纬度 + // 'city_name' => '', //发单城市 用来校验是否跨城;请填写城市的中文名称,如北京市、深圳市 + ], + /*'shop' => [ //发货店铺信息;obj,详见shop结构,平台级开发者需要传入 + 'shop_name' => '店铺姓名', + 'shop_phone' => '店铺电话', + 'shop_address' => '店铺地址', + 'shop_lng' => '店铺经度', + 'shop_lat' => '店铺纬度', + ],*/ + 'order_detail' => [ // 必填,订单详情 + 'total_price' => 0, //用户订单商品总金额(单位:分) 100 表示1元(最大值为100万, 超过此值则按100万计算) + 'product_type' => 10, //必填,物品类型。1:快餐,2:药品,3:百货,4:脏衣服收,5:干净衣服派,6:生鲜,其它详见文档 + // 'user_money' => 0, // 用户实付商家金额(单位:分) 100 表示1元 + // 'shop_money' => 0, // 商家实收用户金额(单位:分) 100 表示1元 + 'weight_gram' => 0, // 必填,物品重量(单位:克) 100 表示100g + // 'volume_litre' => 0, // 物品体积(单位:升) 1 表示1升 + // 'delivery_money' => 0, // 商家收取用户的配送费(单位:分) 100 表示1元 + 'product_num' => 1, // 必填,物品个数 + 'product_type_num' => 1, // 必填,物品种类个数 + 'product_detail' => [ // 必填,物品种类个数 + [ + 'product_name' => '', // 必填,物品名称 + // 'product_id' => 1, // 物品ID + 'product_num' => 1, // 必填,物品数量 + // 'product_price' => 0, // 物品价格 + // 'product_unit' => 0, // 物品单位 + // 'product_remark' => 0, // 备注 + // 'item_detail' => 0, // 详情 + ], + ], + ], + /*'multi_pickup_info' => [ //多点取货信息 + [ + 'pickup_shop_address' => '取货点地址', // 必填,取货点地址 + 'pickup_shop_phone' => '取货点店铺手机号', // 必填,取货点店铺手机号 + 'pickup_shop_name' => '取货点店铺手机号', // 必填,取货点店铺名称 + 'pickup_lng' => '取货点经度', // 必填,取货点经度 + 'pickup_lat' => '取货点纬度', // 必填,取货点纬度 + 'pickup_products' => '', // 必填,取货点店铺物品信息 + ], + ],*/ + ], $params); + + return $this->send('/open/api/external/createorder', $params); + } + + /** + * 取消订单 + */ + public function cancelOrder(array $params): array + { + $params = array_merge([ + 'dev_id' => $this->dev_id, + 'order_id' => $params['order_id'], + 'shop_id' => $this->shop_id, // 使用商家订单号,需要传入shop_id + 'shop_type' => 1, // 输入shop_id,shop_type需必传 + 'order_type' => 2, // 订单ID类型,1、顺丰订单号 2、商家订单号 + 'cancel_code' => 300, + 'push_time' => time(), + ], $params); + + return $this->send('/open/api/external/cancelorder', $params); + } + + /** + * 获取顺丰同城配送费 + */ + public function getDeliveryCost(array $params): float + { + $res = $this->preCreateOrder($params); + $real_pay_money = $res['result']['real_pay_money'] ?? null; + if ($real_pay_money === null) { + throw new BusinessException(500, '获取配送费失败'); + } + return (float)bcdiv($real_pay_money, 100, 2); + } + + /** + * 获取原始配送费 + */ + public function getOriginDeliveryCost(float $deliveryCost): float + { + return (float)bcadd($deliveryCost,3.50,2); + } + + /** + * 预创建订单 + */ + private function preCreateOrder(array $params): array + { + if (!isset($params['user_lng'], $params['user_lat'], $params['user_address'], $params['weight'], $params['shop'])) { + throw new BusinessException(500, '经度、纬度、收货地址、重量、发货店铺信息不能为空'); + } + $params = array_merge([ + 'dev_id' => $this->dev_id, //必填 + 'shop_id' => $this->shop_id, //必填 + 'shop_type' => 1, //1:顺丰店铺ID 2:接入方店铺ID + 'user_lng' => 0, //必填,用户地址经度 + 'user_lat' => 0, //必填,用户地址纬度 + 'user_address' => '', //必填,用户详细地址 + 'weight' => 0, //必填,物品重量(单位:克) + 'product_type' => 6, //必填,物品类型。1:快餐,2:药品,3:百货,4:脏衣服收,5:干净衣服派,6:生鲜,其它详见文档 + // 'total_price' => 0, //用户订单总金额(单位:分) + 'is_appoint' => 0, //必填,是否是预约单,0:非预约单;1:预约单 + 'appoint_type' => 0, //预约单类型,1:预约单送达单;2:预约单上门单,默认:0 + // 'expect_time' => '', //用户期望送达时间,若传入自此段且时间大于配送时效,则按照预约送达单处理,时间小于配送时效按照立即单处理;appoint_type=1时需必传,秒级时间戳 + // 'expect_pickup_time' => '', //用户期望上门时间,appoint_type=2时需必传,秒级时间戳 + // 'lbs_type' => 2, //坐标类型,1:百度坐标,2:高德坐标 + 'pay_type' => 1, //必填,用户支付方式:1、已支付 0、货到付款 + // 'receive_user_money' => 0, //代收金额,单位:分 + 'is_insured' => 0, //必填,是否保价,0:非保价;1:保价 + 'is_person_direct' => 0, //必填,是否是专人直送订单,0:否;1:是 + // 'declared_value' => 0, //保价金额 + // 'gratuity_fee' => 0, //订单小费,不传或者传0为不加小费,单位分,加小费最低不能少于100分 + 'rider_pick_method' => 1, //物流流向,1:从门店取件送至用户;2:从用户取件送至门店 + 'return_flag' => 511, //1:商品总价格,2:配送距离,4:物品重量,8:起送时间,16:期望送达时间,32:支付费用,64:实际支持金额,128:优惠券总金额,256:结算方式,例如全部返回为填入511 + 'push_time' => time(), //必填,推单时间,秒级时间戳(格林尼治时间) + /*'shop' => [ //发货店铺信息;obj,详见shop结构,平台级开发者需要传入 + 'shop_name' => '店铺姓名', + 'shop_phone' => '店铺电话', + 'shop_address' => '店铺地址', + 'shop_lng' => '店铺经度', + 'shop_lat' => '店铺纬度', + ], + 'multi_pickup_info' => [ //多点取货信息 + 'pickup_shop_address' => '取货点地址', + 'pickup_lng' => '取货点经度', + 'pickup_lat' => '取货点纬度', + ],*/ + ], $params); + + return $this->send('/open/api/external/precreateorder', $params); + } + + /** + * 发送请求 + */ + public function send(string $url, array $params): array + { + $url = $this->host . $url; + $url = $url . (str_contains($url, '?') ? '&sign=' . $this->getSign($params) : '?sign=' . $this->getSign($params)); + + $res = json_decode($this->clientFactory->create()->post($url, ['json' => $params])->getBody()->getContents(), true); + if (!$res || !isset($res['error_code']) || $res['error_code'] != 0) { + throw new BusinessException(500, ($res['error_code'] ?? '') . ':' . ($res['error_msg'] ?? '请求异常')); + } + + return $res; + } + + /** + * 计算请求签名 + */ + private function getSign(array $params): string + { + $post_data = json_encode($params); + $sign_char = $post_data . "&{$this->dev_id}&{$this->dev_key}"; + return base64_encode(md5($sign_char)); + } + + /** + * 前端传上来的送达时间(如:11:30 - 12:00)转顺丰的expect_time等预约参数 + */ + public function deliveryTimeNote2expectTime(string $deliveryTimeNote): array + { + if (!empty($deliveryTimeNote) && $deliveryTimeNote != '尽快送达' && strpos($deliveryTimeNote, '-') !== false) { + $arr = array_map(function ($v) { + return trim($v); + }, explode('-', $deliveryTimeNote)); + + if ($arr[0] && $arr[1]) { + $startTime = strtotime($arr[0]); + $endTime = strtotime($arr[1]); + # 判断是否是有效时间戳 + if ($startTime > 1650000000 && $endTime > 1650000000) { + $sfParams['is_appoint'] = 1; + $sfParams['appoint_type'] = 1; + $sfParams['expect_time'] = floor(($startTime + $endTime) / 2); + } + } + } + return $sfParams ?? []; + } +} \ No newline at end of file diff --git a/config/routes.php b/config/routes.php index a5f6040..da8551f 100644 --- a/config/routes.php +++ b/config/routes.php @@ -9,6 +9,8 @@ declare(strict_types=1); * @contact group@hyperf.io * @license https://github.com/hyperf/hyperf/blob/master/LICENSE */ + +use App\Controller\v3\SfExpressController; use Hyperf\HttpServer\Router\Router; Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@index'); @@ -186,4 +188,13 @@ Router::addGroup('/v3/wechat/',function () { Router::post('notify/online', 'App\Controller\v3\NotifyController@wxminiOnline'); Router::post('notify/offline', 'App\Controller\v3\NotifyController@wxminiOffline'); Router::post('notify/refund', 'App\Controller\v3\NotifyController@wxminiRefund'); +}); + +# 顺丰回调 +Router::addGroup('/v3/sf_express/', function () { + Router::addRoute(['POST', 'PUT'], 'rider_status', [SfExpressController::class, 'riderStatus']); //配送状态更改回调 + Router::addRoute(['POST', 'PUT'], 'rider_recall', [SfExpressController::class, 'riderRecall']); //骑士撤单状态回调 + Router::addRoute(['POST', 'PUT'], 'order_complete', [SfExpressController::class, 'orderComplete']); //订单完成回调 + Router::addRoute(['POST', 'PUT'], 'sf_cancel', [SfExpressController::class, 'sfCancel']); //顺丰原因订单取消回调 + Router::addRoute(['POST', 'PUT'], 'rider_exception', [SfExpressController::class, 'riderException']); //顺丰原因订单取消回调 }); \ No newline at end of file diff --git a/小程序修改 b/小程序修改 new file mode 100644 index 0000000..9520e4f --- /dev/null +++ b/小程序修改 @@ -0,0 +1,3 @@ +/pages/orderSubmit/orderSubmit.vue约490行增加 + shopcart_ids: that.shopcart_ids, + delivery_time_note: that.order_info.appointment_time.distribution[that.time_index].value, \ No newline at end of file