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

333 lines
11 KiB

<?php
/**
* Created by PhpStorm.
* User: travis
* Date: 2019/12/12
* Time: 6:20
*/
namespace api\logic;
use backend\modules\shop\models\ars\Order;
use backend\modules\shop\models\ars\PaymentLog;
use backend\modules\shop\models\ars\RefundLog;
use backend\modules\shop\models\ars\WxPayConfig;
use Yii;
use EasyWeChat\Factory;
use yii\db\Exception;
use yii\helpers\Json;
use yii\httpclient\Client;
use yii\web\BadRequestHttpException;
use yii\base\BaseObject;
use yii\web\NotFoundHttpException;
use yii\web\ServerErrorHttpException;
class WxPaymentLogic extends BaseObject
{
/*支付类型*/
const PAY_TYPE_WEB = 1;
const PAY_TYPE_MINI_PROGRAM = 2;
/*支付状态*/
const STATUS_PAYMENT_WAITING = 0;
const STATUS_PAYMENT_SUCCESS = 1;
/*退款状态*/
const STATUS_REFUND_WAIT = 0; //退款待审核
const STATUS_REFUND_CONFIRM = 1; //退款待确认
const STATUS_REFUND_SUCCESS = 2; //退款成功
const STATUS_REFUND_PORTION = 3; //部分退款
/*发起支付方式*/
const TRADE_TYPE_JS_API = 'JSAPI';
public $appId;
public $mchId;
public $key;
public $certPath;
public $keyPath;
public $notifyUrl;
public $tradeType;
public $payType;
public $app;
public $order;
public $viewAction = 'view';
/**
* @param $payType
* @return mixed
* @throws BadRequestHttpException
* @throws Exception
* 微信统一下单
*/
public function wxPayment($payType)
{
$this->payType = $payType;
$unifyParams = $this->applyPaymentData();
return $this->unify($unifyParams);
}
/**
* @return array
* @throws BadRequestHttpException
* @throws Exception
* 生成支付参数
*/
private function applyPaymentData()
{
$orderId = Yii::$app->request->getBodyParam('order_id');/*int 商品id*/
$paymentAmount = Yii::$app->request->getBodyParam('payment_amount');/*int 商品id*/
$notifyUrl = Yii::$app->request->getBodyParam('notify_url');/*int 商品id*/
if (empty($orderId) || empty($paymentAmount) || empty($notifyUrl)) {
throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
}
$this->tradeType = self::TRADE_TYPE_JS_API;
$this->savePaymentLog($orderId, $paymentAmount, $notifyUrl);
$params = [
'body' => '订单支付',
'out_trade_no' => $orderId,
'total_fee' => round($paymentAmount * 100),
'openid' => Yii::$app->user->identity->wx_openid,
];
return $params;
}
/**
* @param string $orderId
* @param float $paymentAmount
* @param string $notifyUrl
* @throws Exception
* 保存支付信息
*/
private function savePaymentLog($orderId, $paymentAmount, $notifyUrl)
{
$paymentLog = PaymentLog::findOne(['order_id' => $this->order->order_sn]);
if (!$paymentLog) {
$paymentLog = new PaymentLog();
}
$paymentLog->order_id = $orderId;
$paymentLog->payment_amount = $paymentAmount;
$paymentLog->notify_url = $notifyUrl;
$paymentLog->type = $this->payType;
$paymentLog->status = self::STATUS_PAYMENT_WAITING;
if (!$paymentLog->save()) {
throw new Exception(Helper::errorMessageStr($paymentLog->errors));
}
}
protected function getApp()
{
$this->initObject();
$config = [
'app_id' => $this->appId,
'mch_id' => $this->mchId,
'key' => $this->key,
'cert_path' => $this->certPath,
'key_path' => $this->keyPath,
'notify_url' => $this->notifyUrl,
'trade_type' => $this->tradeType,
// 'sandbox' => true, // 设置为 false 或注释则关闭沙箱模式
];
$this->app = Factory::payment($config);
// 判断当前是否为沙箱模式:
// $this->app->inSandbox();
}
/**
* @var WxPayConfig $wxPayConfig
*/
private function initObject()
{
$path = Yii::getAlias('@backend');
$wxPayConfig = WxPayConfig::find()->one();
switch ($this->payType) {
case self::PAY_TYPE_WEB:
// $wxConfig = WxConfig::find()->one();
// $this->appId = trim($wxConfig->appid);
break;
case self::PAY_TYPE_MINI_PROGRAM:
// $miniProgramConfig = MiniProgramConfig::find()->one();
// $this->appId = trim($miniProgramConfig->appid);
break;
}
$this->mchId = $wxPayConfig->mch_id;
$this->certPath = trim($path . $wxPayConfig->cert_path);
$this->keyPath = trim($path . $wxPayConfig->key_path);
$this->notifyUrl = Yii::$app->request->hostInfo . '/wx-payment/notify';
}
/**
* @param $unifyParams
* @return mixed
* 统一下单
*/
private function unify($unifyParams)
{
$this->getApp();
return $this->app->order->unify($unifyParams);
}
/**
* @return array|bool
* @throws BadRequestHttpException
* @throws \yii\base\InvalidConfigException
* @throws \yii\httpclient\Exception
* 微信支付回调
*/
public function notify()
{
$notifyData = Json::decode(Json::encode(simplexml_load_string(Yii::$app->request->getRawBody(), 'SimpleXMLElement', LIBXML_NOCDATA)));
Yii::info($notifyData, "notify");
if (!$this->checkSign($notifyData)) {
throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
}
$tra = Yii::$app->db->beginTransaction('SERIALIZABLE');
try {
if ($notifyData->result_code != 'SUCCESS' || $notifyData->return_code != 'SUCCESS') {
throw new BadRequestHttpException('result_code or return_code is false');
}
$paymentLog = PaymentLog::findOne(['order_id' => $notifyData->out_trade_no]);
$this->notifyUrl = Yii::$app->request->hostInfo . $paymentLog->notify_url;
$paymentLog->mch_id = $notifyData->mch_id;
$paymentLog->wx_payment_id = $notifyData->transaction_id; //交易号
$paymentLog->status = self::STATUS_PAYMENT_SUCCESS;
$paymentLog->payment_at = time();
if (!$paymentLog->save()) {
throw new Exception(Helper::errorMessageStr($paymentLog->errors));
}
if (!$tra->commit()) {
throw new Exception('保存数据失败');
}
/*转发回调信息*/
$this->forwardNotify($notifyData, true);
return ['return_code' => 'SUCCESS', 'return_msg' => 'OK'];//回传成功信息到微信服务器
} catch (Exception $e) {
$tra->rollBack();
$this->forwardNotify($notifyData, false);
Yii::info($e->getMessage(), 'notify');
return false;
} catch (BadRequestHttpException $e) {
$tra->rollBack();
$this->forwardNotify($notifyData, false);
Yii::info($e->getMessage(), 'notify');
return false;
}
}
/**
* @param $notifyData $tra->rollBack();
* @param $status
* @return bool
* @throws \yii\base\InvalidConfigException
* @throws \yii\httpclient\Exception
* 转发异步回调信息
*/
private function forwardNotify($notifyData, $status)
{
$notify = [
'notify' => [
'status' => $status,
'notify' => $notifyData
]
];
$client = new Client();
$response = $client->createRequest()
->setMethod('POST')
->setUrl($this->notifyUrl)
->addHeaders(['content-type' => 'application/json'])
->setContent(Json::encode($notify))
->send();
if ($response->isOk) {
return true;
} else {
return false;
}
}
/**
* @return bool
* @throws BadRequestHttpException
* @throws Exception
* @throws NotFoundHttpException
* 申请退款
*/
public function applyRefund()
{
$orderId = Yii::$app->request->getBodyParam('order_id');
$refundId = Yii::$app->request->getBodyParam('wx_refund_id');
$refundAmount = Yii::$app->request->getBodyParam('refund_amount');
$refundAccount = Yii::$app->request->getBodyParam('refund_account');
$reason = Yii::$app->request->getBodyParam('reason');
if (empty($orderId) || empty($refundId) || empty($refundAmount) || empty($reason)) {
throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
}
$paymentLog = PaymentLog::findOne(['order_id' => $orderId]);
if (empty($paymentLog)) {
throw new NotFoundHttpException('订单支付信息未找到');
}
if (RefundLog::findOne(['order_id' => $orderId, 'status' => self::STATUS_REFUND_WAIT])) {
throw new BadRequestHttpException('此订单存在等待审核的退款申请');
}
$refundedAmount = RefundLog::find()
->where(['order_id' => $orderId, 'status' => self::STATUS_PAYMENT_SUCCESS])
->sum('refund_amount') ?? 0;
$refundLog = new RefundLog();
$refundLog->order_id = $orderId;
$refundLog->wx_refund_id = Helper::timeRandomNum(3, 'P');
$refundLog->reason = $reason;
$refundLog->order_amount = $paymentLog->payment_amount;
$refundLog->refund_amount = $refundAmount;
$refundLog->refunded_amount = $refundedAmount;
$refundLog->type = $paymentLog->type;
$refundLog->status = self::STATUS_REFUND_WAIT;
$refundLog->refund_account = $refundAccount;
$refundLog->applyed_at = time();
if (!$refundLog->save()) {
throw new Exception(Helper::errorMessageStr($refundLog->errors));
}
return true;
}
/**
* @param $data
* @return bool
* 支付成功回调验证签名和支付金额
*/
public function checkSign($data)
{
$this->initObject();
$notifySign = $data['sign'];
unset($data['sign']);
$sign = $this->_sign($data);
if ($notifySign == $sign) {
return true;
} else {
return false;
}
}
/**
* @param $arr
* @return string
* 微信签名方法
*/
private function _sign($arr)
{
$arr = array_filter($arr);
ksort($arr);
$arr['key'] = $this->key;
$queryString = http_build_query($arr);
$queryString = urldecode($queryString);
return strtoupper(md5($queryString));
}
}