Compare commits

...

4 Commits

  1. 1
      .gitignore
  2. 18
      MySQL_change.sql
  3. 37
      app/Controller/v3/OrderOnlineController.php
  4. 223
      app/Controller/v3/SfExpressController.php
  5. 6
      app/Controller/v3/UserAddressController.php
  6. 6
      app/JsonRpc/OrderService.php
  7. 22
      app/Model/v3/Goods.php
  8. 22
      app/Model/v3/GoodsActivity.php
  9. 14
      app/Model/v3/SfExpressOrder.php
  10. 108
      app/Service/v3/Implementations/OrderOnlineService.php
  11. 51
      app/Service/v3/Implementations/UserAddressService.php
  12. 2
      app/Service/v3/Interfaces/UserAddressServiceInterface.php
  13. 262
      app/Service/v3/SfExpress.php
  14. 11
      config/routes.php
  15. 3
      小程序修改

1
.gitignore

@ -15,3 +15,4 @@ vendor/
.vscode/
config/cert/apiclient_cert_2.pem
config/cert/apiclient_key_2.pem
/.user.ini

18
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`;

37
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,8 +139,33 @@ class OrderOnlineController extends BaseController
}else{
$address['tags'][0] = ['id' => 6,'name' => '距离最近'];
}
$distributionPrice = $this->distributionPriceService->do($distance);
$originalPrice = $this->distributionPriceService->original($distance);
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,
@ -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'] = [

223
app/Controller/v3/SfExpressController.php

@ -0,0 +1,223 @@
<?php
namespace App\Controller\v3;
use App\Constants\v3\OrderState;
use App\Controller\BaseController;
use App\Model\v3\Order;
use App\Model\v3\OrderMain;
use Hyperf\DbConnection\Db;
use Psr\Http\Message\ResponseInterface;
/**
* 顺丰同城回调
*/
class SfExpressController extends BaseController
{
private string $dev_id;
private string $dev_key;
// private string $shop_id;
public function __construct()
{
parent::__construct(); //此句不能少,否则$this->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));
}
}

6
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,7 +68,9 @@ class UserAddressController extends BaseController
{
$userAddressId = $this->request->input('user_address_id');
$marketId = $this->request->input('market_id');
$res = $this->userAddressService->getAddressAndDistributionPrice($userAddressId,$marketId);
$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]);
}

6
app/JsonRpc/OrderService.php

@ -5,6 +5,7 @@ namespace App\JsonRpc;
use App\Commons\Log;
use App\Constants\ErrorCode;
use App\Service\SeparateAccountsServiceInterface;
use App\Service\v3\SfExpress;
use Hyperf\DbConnection\Db;
use Hyperf\RpcServer\Annotation\RpcService;
use Hyperf\Di\Annotation\Inject;
@ -76,6 +77,11 @@ class OrderService implements OrderServiceInterface
"message" => ''
];
# 取消顺丰配送
go(function () use ($global_order_id) {
SfExpress::getInstance()->cancelOrder(['order_id' => $global_order_id]);
});
$res = $this->orderService->onlineRefund($global_order_id);
if($res['code'] > 0){
$result['result'] = $res;

22
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
}
}

22
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
}
}

14
app/Model/v3/SfExpressOrder.php

@ -0,0 +1,14 @@
<?php
namespace App\Model\v3;
use App\Model\Model;
use Hyperf\Database\Model\Relations\BelongsTo;
class SfExpressOrder extends Model
{
protected $table = 'lanzu_sf_express_orders';
public function orderMain(): BelongsTo
{
return $this->belongsTo(OrderMain::class, 'shop_order_id', 'global_order_id');
}
}

108
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
*/

51
app/Service/v3/Implementations/UserAddressService.php

@ -1,6 +1,5 @@
<?php
namespace App\Service\v3\Implementations;
use App\Constants\v3\ErrorCode;
use App\Exception\ErrorCodeException;
@ -8,9 +7,12 @@ use App\Model\v3\Market;
use App\Model\v3\UserAddress;
use App\Service\v3\Interfaces\DistributionPriceServiceInterface;
use App\Service\v3\Interfaces\LocationServiceInterface;
use App\Service\v3\Interfaces\ShopCartServiceInterface;
use App\Service\v3\Interfaces\UserAddressServiceInterface;
use App\Service\v3\SfExpress;
/** @var Inject 注解使用 */
use Hyperf\Di\Annotation\Inject;
use Hyperf\DbConnection\Db;
class UserAddressService implements UserAddressServiceInterface
{
/**
@ -19,6 +21,12 @@ class UserAddressService implements UserAddressServiceInterface
*/
protected $locationService;
/**
* @Inject
* @var ShopCartServiceInterface
*/
protected $shopCartService;
/**
* @Inject
* @var DistributionPriceServiceInterface
@ -79,19 +87,48 @@ class UserAddressService implements UserAddressServiceInterface
/**
* @param $userAddressId
* @param $marketId
* @return false|float
* @param string $shopCartIds
* @param string $deliveryTimeNote
* @return array
*/
public function getAddressAndDistributionPrice($userAddressId,$marketId)
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);
$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{

2
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);
}

262
app/Service/v3/SfExpress.php

@ -0,0 +1,262 @@
<?php
namespace App\Service\v3;
use App\Exception\BusinessException;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Utils\ApplicationContext;
/**
* 顺丰同城相关
* 文档:https://commit-openic.sf-express.com/open/api/docs/index#/apidoc
*/
class SfExpress
{
private string $dev_id;
private string $dev_key;
private string $shop_id;
private string $host = 'https://openic.sf-express.com';
private ClientFactory $clientFactory;
private static ?SfExpress $_instance = NULL;
public function __construct()
{
$this->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 ?? [];
}
}

11
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');
@ -187,3 +189,12 @@ Router::addGroup('/v3/wechat/',function () {
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']); //顺丰原因订单取消回调
});

3
小程序修改

@ -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,
Loading…
Cancel
Save