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