Compare commits

...

24 Commits

  1. 1
      .gitignore
  2. 79
      MySQL_change.sql
  3. 218
      app/Controller/v3/CcbNotifyController.php
  4. 39
      app/Controller/v3/OrderOnlineController.php
  5. 223
      app/Controller/v3/SfExpressController.php
  6. 8
      app/Controller/v3/UserAddressController.php
  7. 12
      app/JsonRpc/OrderService.php
  8. 65
      app/Model/v3/CcbPayment.php
  9. 22
      app/Model/v3/Goods.php
  10. 22
      app/Model/v3/GoodsActivity.php
  11. 14
      app/Model/v3/SfExpressOrder.php
  12. 642
      app/Service/v3/CcbPaymentService.php
  13. 115
      app/Service/v3/Implementations/OrderOnlineService.php
  14. 63
      app/Service/v3/Implementations/UserAddressService.php
  15. 2
      app/Service/v3/Interfaces/UserAddressServiceInterface.php
  16. 262
      app/Service/v3/SfExpress.php
  17. 19
      config/autoload/ccb.php
  18. 22
      config/routes.php
  19. 100
      test/Cases/CCBTest.php
  20. 3
      小程序修改

1
.gitignore

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

79
MySQL_change.sql

@ -0,0 +1,79 @@
# 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` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`shop_order_id` BIGINT(20) NOT NULL COMMENT '商户订单号,即对应lanzu_order_main的global_order_id',
`sf_order_id` VARCHAR(50) NOT NULL COMMENT '顺丰订单号' COLLATE 'utf8mb4_unicode_ci',
`sf_bill_id` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '顺丰运单号' COLLATE 'utf8mb4_unicode_ci',
`total_price` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '配送费总额,单位:分',
`delivery_distance_meter` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '顺丰返回的配送距离,单位:米',
`weight_gram` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '商品重量,单位:克',
`start_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '起送时间',
`expect_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '预计送达时间',
`total_pay_money` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '支付费用,单位:分',
`real_pay_money` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '实际支付金额,单位:分(实际支付金额=总金额-优惠券总金额)',
`created_at` INT(10) UNSIGNED NULL DEFAULT NULL,
`updated_at` INT(10) UNSIGNED NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `shop_order_id` (`shop_order_id`) USING BTREE
)
COMMENT='顺丰同城订单记录'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB;
# 2022-8-21 12:25:35 门店表增加建行商户编号
ALTER TABLE `lanzu_store` ADD `ccb_merchant_id` VARCHAR(40) NOT NULL DEFAULT '' COMMENT '建行商户编号' AFTER `cash_code_img`;
# 2022-8-21 13:57:15 主订单表增加建行支付类型,增加是否已分润字段
ALTER TABLE `lanzu_order_main`
CHANGE `pay_type` `pay_type` INT(11) NOT NULL DEFAULT '1' COMMENT '1.微信支付2.余额支付3.积分支付4.货到付款5.建行支付',
ADD `is_proft_sharing` TINYINT NOT NULL DEFAULT '0' COMMENT '是否已分润:0否,1是' AFTER `pay_time`;
# 2022-8-21 15:38:35 建行支付订单表
CREATE TABLE `lanzu_ccb_payment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_main_id` bigint(20) NOT NULL COMMENT '主表订单id',
`main_ordr_no` varchar(50) CHARACTER SET utf8mb4 NOT NULL COMMENT '主订单编号',
`pymd_cd` char(2) CHARACTER SET utf8mb4 NOT NULL COMMENT '支付方式:03移动端H5页面,05微信小程序',
`clrg_dt` varchar(8) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '确认收货日期',
`sub_appid` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '小程序的appid',
`sub_openid` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户openid',
`py_ordr_tpcd` char(2) CHARACTER SET utf8mb4 NOT NULL COMMENT '订单类型:04普通订单',
`ordr_tamt` decimal(19,2) NOT NULL COMMENT '订单总金额',
`txn_tamt` decimal(19,2) NOT NULL COMMENT '消费者实付总金额',
`orderlist` json NOT NULL COMMENT '子订单列表(分账明细)',
`py_trn_no` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '银行支付流水号',
`prim_ordr_no` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '银行订单编号',
`ordr_gen_tm` char(14) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '订单生成时间',
`cshdk_url` varchar(300) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT 'H5收银台URL',
`rtn_par_data` json DEFAULT NULL COMMENT '小程序支付参数',
`ordr_stcd` varchar(2) CHARACTER SET utf8mb4 NOT NULL DEFAULT '0' COMMENT '主订单状态:0-初始 1-待支付 2-支付成功3-支付失败 4-全额退款 5-部分退款 6-订单失效 7-待退款',
`rtn_orderlist` json DEFAULT NULL COMMENT '返回的子订单列表',
`pay_time` varchar(14) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支付时间',
`pay_type` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支付方式',
`pay_channel` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支付渠道',
`debit_credit_type` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '借贷记标识',
`created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `main_ordr_no` (`main_ordr_no`),
KEY `order_main_id` (`order_main_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='建行支付订单';

218
app/Controller/v3/CcbNotifyController.php

@ -0,0 +1,218 @@
<?php
namespace App\Controller\v3;
use App\Constants\v3\OrderState;
use App\Controller\BaseController;
use App\Exception\BusinessException;
use App\Model\v3\CcbPayment;
use App\Model\v3\Order;
use App\Service\v3\CcbPaymentService;
use App\Service\v3\Interfaces\BadgeServiceInterface;
use App\Service\v3\Interfaces\CouponRebateServiceInterface;
use App\Service\v3\Interfaces\DeviceServiceInterface;
use App\Service\v3\Interfaces\FeiePrintServiceInterface;
use App\Service\v3\Interfaces\MiniprogramServiceInterface;
use App\Service\v3\Interfaces\MqttServiceInterface;
use App\Service\v3\Interfaces\OrderOnlineServiceInterface;
use App\Service\v3\Interfaces\OrderStatisticsServiceInterface;
use App\Service\v3\Interfaces\SeparateAccountsServiceInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Utils\ApplicationContext;
class CcbNotifyController extends BaseController
{
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @Inject
* @var MqttServiceInterface
*/
protected $mqttService;
/**
* @Inject
* @var DeviceServiceInterface
*/
protected $deviceService;
/**
* @Inject
* @var MiniprogramServiceInterface
*/
protected $miniprogramService;
/**
* @Inject
* @var FeiePrintServiceInterface
*/
protected $feiePrintService;
/**
* @Inject
* @var CouponRebateServiceInterface
*/
protected $couponRebateService;
/**
* @Inject
* @var OrderOnlineServiceInterface
*/
protected $orderOnlineService;
/**
* @Inject
* @var SeparateAccountsServiceInterface
*/
protected $separateAccountsService;
/**
* @Inject
* @var BadgeServiceInterface
*/
protected $badgeService;
/**
* @Inject
* @var OrderStatisticsServiceInterface
*/
protected $orderStatisticsService;
public function __construct(LoggerFactory $loggerFactory)
{
parent::__construct();
$this->logger = $loggerFactory->get('ccb.notify');
}
private function saveLog($func, $content)
{
$this->logger->info($func."\n".$content."\n");
}
public function pay()
{
try {
$this->saveLog(__FUNCTION__, $this->request->getBody()->getContents());
$data = $this->request->all();
if (!isset($data['Main_Ordr_No'], $data['Sign_Inf'])) {
throw new BusinessException(500, '缺少参数');
}
$ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
if (!$ccb->verifySign($ccb->createSign($data), $data['Sign_Inf'])) {
throw new BusinessException(500, '验签失败');
}
$model = CcbPayment::where('main_ordr_no', $data['Main_Ordr_No'])->first();
if (!$model) {
throw new BusinessException(500, '下单记录不存在');
}
$payResult = $ccb->gatherEnquireOrder($data['Main_Ordr_No']);
if ($payResult['Ordr_Stcd'] != $data['Ordr_Stcd']) {
throw new BusinessException(500, '订单状态不一致');
}
// 状态已同步
if ($model->ordr_stcd == $data['Ordr_Stcd']) {
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
}
$model->ordr_stcd = $data['Ordr_Stcd'];
$model->pay_time = $data['Pay_Time'];
$model->pay_type = $data['TYPE'];
$model->pay_channel = $data['PAY_CHANNEL'];
$model->debit_credit_type = $data['DEBIT_CREDIT_TYPE'];
$model->save();
if ($payResult['Ordr_Stcd'] == '2') {
$orderMain = $model->orderMain;
if ($orderMain->state != OrderState::UNPAID) {
throw new BusinessException(500, '订单状态异常');
}
$this->orderOnlineService->doByPaid($orderMain->global_order_id);
$this->separateAccountsService->orderOnlinePaid($orderMain->global_order_id);
//记录当前市场的当天外卖订单数
$this->orderStatisticsService->setForMarket($orderMain->market_id);
// 优惠券返券
$this->couponRebateService->couponRebateInTask($orderMain->global_order_id);
// 喇叭通知,兼容旧音响,MQTT+IOT
$res = $this->mqttService->speakToStore($orderMain->global_order_id);
$res = $this->deviceService->pubMsgToStoreByOrderMainId($orderMain->global_order_id);
// 打印订单,自动打印
$res = $this->feiePrintService->feiePrint($orderMain->global_order_id);
// 记录badge
$orderChildIds = Order::query()->where(['order_main_id' => $orderMain->global_order_id])->pluck('store_id');
$this->badgeService->doByOrder($orderMain->user_id, $orderChildIds, $orderMain->global_order_id, OrderState::PAID);
// 公众号模板消息
$res = $this->miniprogramService->sendTemMsgForOnlineOrder($orderMain->global_order_id);
}
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
} catch (\Exception $e) {
return $this->response->json([
'Svc_Rsp_St' => '01',
'Rsp_Inf' => $e instanceof BusinessException ? $e->getMessage() : '内部错误'
]);
}
}
public function refund()
{
$this->saveLog(__FUNCTION__, $this->request->getBody()->getContents());
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
}
public function merchant()
{
$this->saveLog(__FUNCTION__, $this->request->getBody()->getContents());
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
}
public function accounting()
{
$this->saveLog(__FUNCTION__, $this->request->getBody()->getContents());
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
}
public function bill()
{
$this->saveLog(__FUNCTION__, $this->request->getBody()->getContents());
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
}
public function platform()
{
$this->saveLog(__FUNCTION__, $this->request->getBody()->getContents());
return $this->response->json([
'Svc_Rsp_St' => '00',
]);
}
}

39
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'] = [

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

8
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(){

12
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;
@ -73,9 +74,14 @@ class OrderService implements OrderServiceInterface
"status" => 200,
"code" => ErrorCode::ORDER_FAILURE,
"result" => [],
"message" => ''
"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;
@ -85,7 +91,7 @@ class OrderService implements OrderServiceInterface
$result['result'] = $res;
$result['message'] = '退款成功';
};
return $result;
}
}

65
app/Model/v3/CcbPayment.php

@ -0,0 +1,65 @@
<?php
declare (strict_types=1);
namespace App\Model\v3;
use Hyperf\DbConnection\Model\Model;
/**
* @property int $id
* @property int $order_main_id
* @property string $main_ordr_no
* @property string $pymd_cd
* @property string $clrg_dt
* @property string $sub_appid
* @property string $sub_openid
* @property string $py_ordr_tpcd
* @property float $ordr_tamt
* @property float $txn_tamt
* @property string $orderlist
* @property string $py_trn_no
* @property string $prim_ordr_no
* @property string $ordr_gen_tm
* @property string $cshdk_url
* @property string $rtn_par_data
* @property string $ordr_stcd
* @property string $rtn_orderlist
* @property string $pay_time
* @property string $pay_type
* @property string $pay_channel
* @property string $debit_credit_type
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class CcbPayment extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'lanzu_ccb_payment';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'order_main_id' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function orderMain()
{
return $this->belongsTo(OrderMain::class, 'order_main_id', 'global_order_id');
}
}

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

642
app/Service/v3/CcbPaymentService.php

@ -0,0 +1,642 @@
<?php
namespace App\Service\v3;
use App\Commons\Log;
use App\Constants\v3\ErrorCode;
use App\Constants\v3\LogLabel;
use App\Exception\BusinessException;
use App\Exception\ErrorCodeException;
use App\Model\v3\CcbPayment;
use App\Model\v3\OrderMain;
use App\Model\v3\User;
use App\TaskWorker\SSDBTask;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Utils\ApplicationContext;
class CcbPaymentService
{
/**
* 是否测试环境
* @var bool
*/
private $isDebug;
/**
* 测试环境请求的域名
* @var string
*/
private $devBaseUri = 'http://marketpayktwo.dev.jh:8028';
/**
* 生产环境请求的域名
* @var string
*/
private $prodBaseUri = 'https://marketpay.ccb.com';
/**
* 市场编号
* @var string
*/
private $mktId;
/**
* 我方商家编号
* @var string
*/
private $merchantId;
/**
* 支付方式
* @var string
*/
private $pymdCd;
/**
* 我方私钥
* @var resource
*/
private $selfPrivateKey;
/**
* 银行公钥
* @var resource
*/
private $bankPublicKey;
/**
* 发起渠道编号
* @var string
*/
private $ittpartyStmId = '00000';
/**
* 支付渠道代码
* @var string
*/
private $pyChnlCd = '0000000000000000000000000';
/**
* @var \Hyperf\Guzzle\ClientFactory
*/
private $clientFactory;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* @Inject
* @var Log
*/
protected $log;
public function __construct(ClientFactory $clientFactory, LoggerFactory $loggerFactory)
{
$this->clientFactory = $clientFactory;
$this->logger = $loggerFactory->get('ccb');
$this->isDebug = config('ccb.debug');
$this->mktId = config('ccb.mkt_id');
$this->merchantId = config('ccb.merchant_id');
$this->pymdCd = config('ccb.pymd_cd');
$selfPrivateKey = config('ccb.self_private_key');
$selfPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($selfPrivateKey, 64, "\n")."-----END RSA PRIVATE KEY-----\n";
$this->selfPrivateKey = openssl_get_privatekey($selfPrivateKey);
$bankPublicKey = config('ccb.bank_public_key');
$bankPublicKey = "-----BEGIN PUBLIC KEY-----\n".chunk_split($bankPublicKey, 64, "\n")."-----END PUBLIC KEY-----\n";
$this->bankPublicKey = openssl_get_publickey($bankPublicKey);
}
/**
* 使用我方私钥加密
* @param string $data
* @return string
*/
public function encrypt(string $data)
{
$str = '';
foreach (str_split($data, 117) as $chunk) {
openssl_private_encrypt($chunk, $crypted, $this->selfPrivateKey);
$str .= $crypted;
}
return base64_encode($str);
}
/**
* 使用银行公钥解密
* @param string $data
* @return string
*/
public function decrypt(string $data)
{
$raw = base64_decode($data);
$str = '';
foreach (str_split($raw, 256) as $chunk) {
openssl_public_decrypt($chunk, $decrypted, $this->bankPublicKey);
$str .= $decrypted;
}
return $str;
}
/**
* 使用我方私钥生成签名
* @param string $data
* @return string
*/
public function sign(string $data)
{
openssl_sign($data, $signature, $this->selfPrivateKey, OPENSSL_ALGO_SHA256);
return base64_encode($signature);
}
/**
* 使用银行公钥验证签名
* @param string $data
* @param string $signature
* @return bool
*/
public function verifySign(string $data, string $signature)
{
$signature = base64_decode($signature);
$result = openssl_verify($data, $signature, $this->bankPublicKey, OPENSSL_ALGO_SHA256);
return $result == 1;
}
/**
* 计算待签名字符串
* @param array $params
* @return string
*/
public function createSign(array $params)
{
// 不参与签名的字符串
$unsignKeys = ['Sign_Inf', 'Svc_Rsp_St', 'Svc_Rsp_Cd', 'Rsp_Inf'];
$result = [];
ksort($params);
foreach ($params as $key => $item) {
if (in_array($key, $unsignKeys)) {
continue;
}
if (is_array($item)) {
foreach ($item as $child) {
$value = $this->createSign($child);
if ($value !== '') {
$result[] = ['', $value];
}
}
} else {
$value = trim($item);
if ($value !== '') {
$result[] = [$key, $value];
}
}
}
$str = '';
foreach ($result as [$key, $value]) {
$str .= $key ? "$key=$value&" : "$value&";
}
return rtrim($str, '&');
}
/**
* 发送API请求
* @param string $uri
* @param array $params
* @return array
*/
private function apiRequest(string $uri, array $params = []): array
{
$signData = $this->createSign($params);
$params['Sign_Inf'] = $this->sign($signData);
if ($this->isDebug) {
$uri = $this->devBaseUri.$uri;
} else {
$uri = $this->prodBaseUri.$uri;
}
$options = ['json' => $params];
$startTime = microtime(true);
try {
$response = $this->clientFactory->create(['timeout' => 60])->post($uri, $options);
} catch (\Exception $e) {
$this->saveApiLog(0, $uri, $params, $e->getMessage());
throw new BusinessException(500, '请求异常');
}
$useTime = round((microtime(true) - $startTime) * 1000, 2);
$content = $response->getBody()->getContents();
$result = json_decode($content, true);
if (!isset($result['Svc_Rsp_St']) || $result['Svc_Rsp_St'] != '00') {
$this->saveApiLog($useTime, $uri, $params, $content);
throw new BusinessException(500, (($result['Rsp_Inf'] ?? '') ?: 'CCB请求失败'));
}
if (!isset($result['Sign_Inf']) || !$this->verifySign($this->createSign($result), $result['Sign_Inf'])) {
$this->saveApiLog($useTime, $uri, $params, $content);
throw new BusinessException(500, (($result['Rsp_Inf'] ?? '') ?: 'CCB验签失败'));
}
if ($this->isDebug) {
$this->saveApiLog($useTime, $uri, $params, $content);
}
return $result;
}
/**
* 保存API请求日志
* @param $useTime
* @param $uri
* @param $params
* @param $content
* @return void
*/
private function saveApiLog($useTime, $uri, $params, $content)
{
$this->logger->info(
sprintf(
"%s\nTime: %.2f ms\nUrl: %s\nParams:\n%s\nContent:\n%s\n",
'CCB API Log',
$useTime,
$uri,
json_encode($params, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
$content
)
);
}
private function getTimestamp()
{
return (new \DateTime())->format('YmdHisv');
}
private function genSerialNumber()
{
return date('YmdHis').mt_rand(10000, 99999).mt_rand(10000, 99999);
}
private function genMainOrderNo()
{
return time().mt_rand(10000, 99999).mt_rand(10000, 99999);
}
/**
* 3.1 生成支付订单接口
* @param string $Main_Ordr_No 主订单编号
* @param string $Ordr_Tamt 订单总金额
* @param string $Txn_Tamt 交易总金额(实付)
* @param string|null $Sub_Appid 当前调起支付的小程序appid
* @param string|null $Sub_Openid 用户在小程序下的openid
* @param array $Orderlist 子订单列表
* @return array
*/
public function gatherPlaceorder(string $Main_Ordr_No, string $Ordr_Tamt, string $Txn_Tamt, ?string $Sub_Appid, ?string $Sub_Openid, array $Orderlist)
{
$uri = '/online/direct/gatherPlaceorder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Main_Ordr_No' => $Main_Ordr_No,
'Pymd_Cd' => $this->pymdCd,
'Py_Ordr_Tpcd' => '04',
'Py_Rslt_Ntc_Sn' => '1',
'Ccy' => '156',
'Ordr_Tamt' => $Ordr_Tamt,
'Txn_Tamt' => $Txn_Tamt,
'Order_Time_Out' => '1800',
'Orderlist' => $Orderlist,
'Vno' => '4',
];
if ($this->pymdCd == '05') {
$params['Sub_Appid'] = $Sub_Appid;
$params['Sub_Openid'] = $Sub_Openid;
}
return $this->apiRequest($uri, $params);
}
/**
* 下单子订单格式化
* @param string $Mkt_Mrch_Id 商家编号
* @param string $Cmdty_Ordr_No 子订单编号
* @param string $Ordr_Amt 订单金额
* @param string $Txnamt 实付金额
* @param string $Cmdty_Dsc 商品描述
* @return string[]
*/
public function subOrderListItem(string $Mkt_Mrch_Id, string $Cmdty_Ordr_No, string $Ordr_Amt, string $Txnamt, string $Cmdty_Dsc)
{
return compact('Mkt_Mrch_Id', 'Cmdty_Ordr_No', 'Ordr_Amt', 'Txnamt', 'Cmdty_Dsc');
}
/**
* 3.4 查询支付结果接口,同一笔订单不支持并发查询
* @param string $Main_Ordr_No 主订单号,主订单号与支付流水号必输其一
* @param string $Py_Trn_No 银行支付流水号
* @return array
*/
public function gatherEnquireOrder(string $Main_Ordr_No, string $Py_Trn_No = '')
{
$uri = '/online/direct/gatherEnquireOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Main_Ordr_No' => $Main_Ordr_No,
'Py_Trn_No' => $Py_Trn_No,
'Vno' => '4',
];
$ssdb = ApplicationContext::getContainer()->get(SSDBTask::class);
$key = 'ccb_'.__FUNCTION__.'_'.$Main_Ordr_No.$Py_Trn_No;
if ($ssdb->exec('setnx',$key, 1)) {
try {
return $this->apiRequest($uri, $params);
} finally {
$ssdb->exec('expire', $key, 2);
}
} else {
sleep(1);
return $this->gatherEnquireOrder($Main_Ordr_No, $Py_Trn_No);
}
}
/**
* 4.1 订单退款接口
* @param string $Cust_Rfnd_Trcno 退款流水号,对于同一笔退款唯一
* @param string $Py_Trn_No 银行支付流水号
* @param string $Rfnd_Amt 退款金额,全额退款时可不传
* @param array $Sub_Ordr_List 子订单列表,全额退款时不需要传该域
* @return array
*/
public function refundOrder(string $Cust_Rfnd_Trcno, string $Py_Trn_No, string $Rfnd_Amt = '', array $Sub_Ordr_List = [])
{
$uri = '/online/direct/refundOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
'Py_Trn_No' => $Py_Trn_No,
'Rfnd_Amt' => $Rfnd_Amt,
'Sub_Ordr_List' => $Sub_Ordr_List,
'Vno' => '3',
];
if ($Sub_Ordr_List) {
$params['Sub_Ordr_List'] = $Sub_Ordr_List;
}
return $this->apiRequest($uri, $params);
}
/**
* 退款子订单格式化
* @param string $Sub_Ordr_Id 银行子订单编号
* @param string $Rfnd_Amt 退款金额
* @return string[]
*/
public function refundSubOrderListItem(string $Sub_Ordr_Id, string $Rfnd_Amt)
{
return compact('Sub_Ordr_Id', 'Rfnd_Amt');
}
/**
* 4.3 查询退款结果接口,同一笔退款不支持并发查询
* @param string $Cust_Rfnd_Trcno 我方退款流水号,与银行退款流水号必输其一
* @param string $Rfnd_Trcno 银行退款流水号
* @return array
*/
public function enquireRefundOrder(string $Cust_Rfnd_Trcno, string $Rfnd_Trcno = '')
{
$uri = '/online/direct/enquireRefundOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Cust_Rfnd_Trcno' => $Cust_Rfnd_Trcno,
'Rfnd_Trcno' => $Rfnd_Trcno,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
/**
* 5.3 确认收货接口
* @param string $Prim_Ordr_No 银行主订单编号
* @return array
*/
public function mergeNoticeArrival(string $Prim_Ordr_No)
{
$uri = '/online/direct/enquireRefundOrder';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Prim_Ordr_No' => $Prim_Ordr_No,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
/**
* 10.1 订单信息查询
* @param string $Main_Ordr_No 主订单编号
* @param string $Py_Trn_No 支付流水号,主订单号与支付流水号必输其一
* @return array
*/
public function orderInfQuery(string $Main_Ordr_No, string $Py_Trn_No = '')
{
$uri = '/online/direct/OrderInfQuery';
$params = [
'Ittparty_Stm_Id' => $this->ittpartyStmId,
'Py_Chnl_Cd' => $this->pyChnlCd,
'Ittparty_Tms' => $this->getTimestamp(),
'Ittparty_Jrnl_No' => $this->genSerialNumber(),
'Mkt_Id' => $this->mktId,
'Main_Ordr_No' => $Main_Ordr_No,
'Py_Trn_No' => $Py_Trn_No,
'Vno' => '4',
];
return $this->apiRequest($uri, $params);
}
/**
* 创建支付订单
* @param $globalOrderId
* @param $userId
* @return array
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function createCcbPayment($globalOrderId, $userId)
{
try {
// 待支付的,未超时(15min,900sec)的订单
$orderMain = OrderMain::query()
->where(['state' => OrderState::UNPAID, 'global_order_id' => $globalOrderId, 'user_id' => $userId])
->where('created_at', '>=', (time() - 900))
->first();
if (empty($orderMain)) {
throw new BusinessException(ErrorCode::ORDER_NOT_AVAILABLE, '[订单号无效]'.$globalOrderId);
}
$user = User::select('openid')->find($userId);
$model = new CcbPayment();
$model->order_main_id = $orderMain->global_order_id;
$model->main_ordr_no = $this->genMainOrderNo();
$model->pymd_cd = $this->pymdCd;
if ($this->pymdCd == '05') {
$model->sub_appid = config('wechat.applet.app_id');
$model->sub_openid = $user->openid;
}
$model->py_ordr_tpcd = '04';
$model->ordr_tamt = $orderMain->money;
$model->txn_tamt = $orderMain->money;
$subOrderList = [];
// 运费判断
if (bccomp($orderMain->delivery_money, '0', 2) == 1) {
$subOrderList[] = $this->subOrderListItem(
$this->merchantId,
$model->main_ordr_no.'D',
$orderMain->delivery_money,
$orderMain->delivery_money,
'配送费'
);
}
// 我方分润比例%
$selfProfitRatio = config('ccb.self_profit_ratio');
if (bccomp($selfProfitRatio, '0', 3) == 0) {
throw new BusinessException(500, '[未配置分润比例]');
}
foreach ($orderMain->orders as $order) {
if (empty($order->store->ccb_merchant_id)) {
throw new BusinessException(500, '[店铺未配置商家编号]'.$order->store_id);
}
foreach ($order->orderGoods as $orderGoods) {
$goodsMoney = bcmul($orderGoods->price, (string)$orderGoods->number, 2);
$selfProfitMoney = bcmul($goodsMoney, $selfProfitRatio, 2);
$merchantProfitMoney = bcsub($goodsMoney, $selfProfitMoney, 2);
// 平台抽佣后最小金额不能为0
if (bccomp($merchantProfitMoney, '0', 2) == 0) {
$selfProfitMoney = '0.00';
$merchantProfitMoney = $goodsMoney;
}
$subOrderList[] = $this->subOrderListItem(
$order->store->ccb_merchant_id,
$model->main_ordr_no.'G'.$orderGoods->id.'N01',
$merchantProfitMoney,
$merchantProfitMoney,
$orderGoods->name.'(分佣到账)'
);
if (bccomp($selfProfitMoney, '0', 2) == 1) {
$subOrderList[] = $this->subOrderListItem(
$this->merchantId,
$model->main_ordr_no.'G'.$orderGoods->id.'N02',
$selfProfitMoney,
$selfProfitMoney,
$orderGoods->name.'(平台抽佣)'
);
}
}
}
$model->orderlist = json_encode($subOrderList);
$model->save();
$result = $this->gatherPlaceorder(
$model->main_ordr_no,
$model->ordr_tamt,
$model->txn_tamt,
$model->sub_appid,
$model->sub_openid,
$subOrderList
);
$model->py_trn_no = $result['Py_Trn_No'];
$model->prim_ordr_no = $result['Prim_Ordr_No'];
$model->ordr_gen_tm = $result['Ordr_Gen_Tm'];
if (isset($result['Cshdk_Url'])) {
$model->cshdk_url = $result['Cshdk_Url'];
}
if ($this->pymdCd == '05') {
$model->rtn_par_data = json_encode($result['Rtn_Par_Data']);
}
$model->ordr_stcd = $result['Ordr_Stcd'];
$model->rtn_orderlist = json_encode($result['Orderlist']);
$model->save();
if ($this->pymdCd == '05') {
// 返回支付参数给前端
$parameters = [
'appId' => $result['Rtn_Par_Data']['appId'],
'timeStamp' => $result['Rtn_Par_Data']['timeStamp'],
'nonceStr' => $result['Rtn_Par_Data']['nonceStr'],
'package' => $result['Rtn_Par_Data']['package'],
'signType' => $result['Rtn_Par_Data']['signType'],
'paySign' => $result['Rtn_Par_Data']['paySign'],
];
} else {
$parameters = [
'Cshdk_Url' => $model->cshdk_url,
];
}
$parameters['order_main_id'] = $orderMain->global_order_id;
return $parameters;
} catch (\Exception $e) {
$this->log->event(LogLabel::ORDER_PAYMENT_LOG, ['payment_do_exception_msg' => $e->getMessage()]);
$message = $e instanceof BusinessException ? $e->getMessage() : '[稍后重试]';
throw new ErrorCodeException(ErrorCode::PAYMENT_FAIL, $message);
}
}
}

115
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,15 +15,16 @@ 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;
use App\Model\v3\UserAddress;
use App\Service\v3\CcbPaymentService;
use App\Service\v3\Interfaces\BadgeServiceInterface;
use App\Service\v3\Interfaces\CouponRecServiceInterface;
use App\Service\v3\Interfaces\CouponServiceInterface;
@ -36,10 +36,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 +138,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 +163,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 +313,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 增加商品重量
];
}
@ -471,7 +474,11 @@ class OrderOnlineService implements OrderOnlineServiceInterface
$this->badgeService->doByOrder($userId, array_values(array_column($dataChildren, 'store_id')), $orderMain->global_order_id, OrderState::UNPAID);
// 支付
return $this->paymentService->do($globalOrderId, $totalAmount, $userId, config('wechat.notify_url.online'));
//return $this->paymentService->do($globalOrderId, $totalAmount, $userId, config('wechat.notify_url.online'));
// 建行支付
$ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
return $ccb->createCcbPayment($globalOrderId, $userId);
} catch (Exception $e) {
Db::rollBack();
@ -651,6 +658,9 @@ class OrderOnlineService implements OrderOnlineServiceInterface
Db::commit();
# 创建顺丰订单
$this->createSfExpressOrder($globalOrderId);
// 释redis库存
$redis = ApplicationContext::getContainer()->get(Redis::class);
foreach ($orderGoods as $k => &$goodsItem) {
@ -697,6 +707,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
*/

63
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
@ -76,23 +84,52 @@ class UserAddressService implements UserAddressServiceInterface
return UserAddress::query()->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';

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 ?? [];
}
}

19
config/autoload/ccb.php

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
return [
'debug' => env('CCB_DEBUG', false),
'pymd_cd' => env('CCB_PYMD_CD', ''),
'mkt_id' => env('CCB_MKT_ID', ''),
'merchant_id' => env('CCB_MERCHANT_ID', ''),
'self_profit_ratio' => env('CCB_SELF_PROFIT_RATIO', 0),
'self_private_key' => env('CCB_SELF_PRIVATE_KEY', ''),
'bank_public_key' => env('CCB_BANK_PUBLIC_KEY', ''),
'notify_url' => [
'online' => env('SITE_HOST').'/v3/ccb/notify/pay',
//'offline' => env('SITE_HOST') . '/v3/ccb/notify/offline',
'refund' => env('SITE_HOST').'/v3/ccb/notify/refund',
],
];

22
config/routes.php

@ -9,6 +9,9 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use App\Controller\v3\CcbNotifyController;
use App\Controller\v3\SfExpressController;
use Hyperf\HttpServer\Router\Router;
Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@index');
@ -186,4 +189,23 @@ 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']); //顺丰原因订单取消回调
});
// 建行支付回调
Router::addGroup('/v3/ccb/',function () {
Router::post('notify/pay', [CcbNotifyController::class, 'pay']);
Router::post('notify/refund', [CcbNotifyController::class, 'refund']);
Router::post('notify/merchant', [CcbNotifyController::class, 'merchant']);
Router::post('notify/accounting', [CcbNotifyController::class, 'accounting']);
Router::post('notify/bill', [CcbNotifyController::class, 'bill']);
Router::post('notify/platform', [CcbNotifyController::class, 'platform']);
});

100
test/Cases/CCBTest.php

@ -0,0 +1,100 @@
<?php
namespace HyperfTest\Cases;
use App\Service\v3\CcbPaymentService;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\HttpTestCase;
class CCBTest extends HttpTestCase
{
// public function testSign()
// {
// $ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
//
// $data = "Write Once, Run Anywhere";
//
// $result = $ccb->sign($data);
//
// $s = 'odUeD1V6obC/j8lUvmFwn6LSQ2DrvaDEin5DKs0FiB/HttboPJncmLisH22Y1grPezE0a+Ij6cdd5Taof8e4A76pKdXA+hDGz0nPMlCRgSF5tHQ6uVXktL/3lqpVTX6ECjRoHmzmo6cAMqVXYQKEl56r4gJcBPW4X4ghRtTgw9AK8+8b8O5EAqxuPxMyhSOYrCsUYRXapezV3uioEReYjvhg/u+kRf662P2nL0ab3szGMXMAoE+JjgMAHR9WCL3Can/5ADgoFff/7kMSxrA7/r94EfkDN50IvRVAx9WBLx3+WWcRRfM6JcqZ91B+g00wYN/OSZHTJDPgV6Ofd5cBpA==';
//
// $this->assertTrue($result == $s);
// }
//
// public function testCreateSign()
// {
// $ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
//
// $params = json_decode('{"Blank3":" ","Blank2":"","Sign_Inf":"signInf","Rsp_Inf":"rspInf","Svc_Rsp_St":"svcRspSt","Svc_Rsp_Cd":"svcRspCd","Amt":"amt","Pymd_Cd":"pymdCd","Parlist":[{"Seq_No":"seqNo","Mkt_Mrch_Id":"mktMrchIdFj"},{"Seq_No":"seqNo","Xbb":[{"sdc":"1131","xyz":"xxxx"}],"Mkt_Mrch_Id":"mktMrchIdFj2"},{"Seq_No":"seqNo3","Mkt_Mrch_Id":"mktMrchIdFj3"}]}', true);
//
// $str = $ccb->createSign($params);
//
// $s = 'Amt=amt&Mkt_Mrch_Id=mktMrchIdFj&Seq_No=seqNo&Mkt_Mrch_Id=mktMrchIdFj2&Seq_No=seqNo&sdc=1131&xyz=xxxx&Mkt_Mrch_Id=mktMrchIdFj3&Seq_No=seqNo3&Pymd_Cd=pymdCd';
//
// $this->assertTrue($str == $s);
// }
// public function testOrder()
// {
// $ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
//
// $subOrderList[] = $ccb->subOrderListItem(
// '41060860800469000000',
// '151201',
// '0.50',
// '0.50'
// );
//
// $subOrderList[] = $ccb->subOrderListItem(
// '41060860800469061877',
// '151202',
// '1.02',
// '1.02'
// );
//
// $subOrderList[] = $ccb->subOrderListItem(
// '41060860800469061878',
// '151203',
// '2.30',
// '2.30'
// );
//
// $result = $ccb->gatherPlaceorder(
// '202208212237008561189348', //$ccb->genSerialNumber(),
// 'm2020060915410278993',
// '3.82',
// '3.82',
// '03',
// '',
// '',
// $subOrderList
// );
//
// var_export($result);
// }
// public function testQuery()
// {
// $ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
//
// $result = $ccb->gatherEnquireOrder('m2020060915410278990');
//
// var_export($result);
// }
//
// public function testOrderInfQuery()
// {
// $ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
//
// $result = $ccb->orderInfQuery('m2020060915410278990', '10500000780012708212236595958H');
//
// var_export($result);
// }
public function testCreateCcbPayment()
{
$ccb = ApplicationContext::getContainer()->get(CcbPaymentService::class);
$resul = $ccb->createCcbPayment('415142026151948289', 193155);
var_export($resul);
}
}

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