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.

267 lines
7.9 KiB

  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: travis
  5. * Date: 2019/12/12
  6. * Time: 6:20
  7. */
  8. namespace api\logic;
  9. use backend\modules\shop\models\ars\PaymentLog;
  10. use backend\modules\shop\models\ars\WxPayConfig;
  11. use Yii;
  12. use EasyWeChat\Factory;
  13. use yii\db\Exception;
  14. use yii\helpers\Json;
  15. use yii\httpclient\Client;
  16. use yii\web\BadRequestHttpException;
  17. use yii\base\BaseObject;
  18. class WxPaymentLogic extends BaseObject
  19. {
  20. /*支付类型*/
  21. const PAY_TYPE_WEB = 1;
  22. const PAY_TYPE_MINI_PROGRAM = 2;
  23. /*发起支付方式*/
  24. const TRADE_TYPE_JS_API = 'JSAPI';
  25. public $appId;
  26. public $mchId;
  27. public $key;
  28. public $certPath;
  29. public $keyPath;
  30. public $notifyUrl;
  31. public $tradeType;
  32. public $payType;
  33. public $app;
  34. public $order;
  35. /**
  36. * @param $payType
  37. * @throws BadRequestHttpException
  38. * @throws Exception
  39. * 微信统一下单
  40. */
  41. public function wxPayment($payType)
  42. {
  43. $this->payType = $payType;
  44. $unifyParams = $this->applyPaymentData();
  45. $this->unify($unifyParams);
  46. }
  47. /**
  48. * @return array
  49. * @throws BadRequestHttpException
  50. * @throws Exception
  51. * 生成支付参数
  52. */
  53. private function applyPaymentData()
  54. {
  55. $orderId = Yii::$app->request->getBodyParam('order_id');/*int 商品id*/
  56. $paymentAmount = Yii::$app->request->getBodyParam('payment_amount');/*int 商品id*/
  57. $notifyUrl = Yii::$app->request->getBodyParam('notify_url');/*int 商品id*/
  58. if (empty($orderId) || empty($paymentAmount) || empty($notifyUrl)) {
  59. throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
  60. }
  61. $this->tradeType = self::TRADE_TYPE_JS_API;
  62. $this->savePaymentLog($orderId, $paymentAmount, $notifyUrl);
  63. $params = [
  64. 'body' => '订单支付',
  65. 'out_trade_no' => $orderId,
  66. 'total_fee' => round($paymentAmount * 100),
  67. 'openid' => Yii::$app->user->identity->wx_openid,
  68. ];
  69. return $params;
  70. }
  71. /**
  72. * @param string $orderId
  73. * @param float $paymentAmount
  74. * @param string $notifyUrl
  75. * @throws Exception
  76. * 保存支付信息
  77. */
  78. private function savePaymentLog($orderId, $paymentAmount, $notifyUrl)
  79. {
  80. $paymentLog = PaymentLog::findOne(['order_id' => $this->order->order_sn]);
  81. if (!$paymentLog) {
  82. $paymentLog = new PaymentLog();
  83. }
  84. $paymentLog->order_id = $orderId;
  85. $paymentLog->payment_amount = $paymentAmount;
  86. $paymentLog->notify_url = $notifyUrl;
  87. $paymentLog->type = $this->payType;
  88. $paymentLog->status = PaymentLog::PAYMENT_STATUS_WAITING;
  89. if (!$paymentLog->save()) {
  90. throw new Exception(Helper::errorMessageStr($paymentLog->errors));
  91. }
  92. }
  93. private function getPaymentApp()
  94. {
  95. $this->initObject();
  96. $config = [
  97. 'app_id' => $this->appId,
  98. 'mch_id' => $this->mchId,
  99. 'key' => $this->key,
  100. 'cert_path' => $this->certPath,
  101. 'key_path' => $this->keyPath,
  102. 'notify_url' => $this->notifyUrl,
  103. 'trade_type' => $this->tradeType,
  104. 'sandbox' => true, // 设置为 false 或注释则关闭沙箱模式
  105. ];
  106. $this->app = Factory::payment($config);
  107. // 判断当前是否为沙箱模式:
  108. $this->app->inSandbox();
  109. }
  110. /**
  111. * @var WxPayConfig $wxPayConfig
  112. */
  113. private function initObject()
  114. {
  115. $path = Yii::getAlias('@backend');
  116. $wxPayConfig = WxPayConfig::find()->one();
  117. // $wxConfig = WxConfig::find()->one();
  118. // switch ($this->payType) {
  119. // case self::PAY_TYPE_WEB:
  120. // $this->appId = trim($wxConfig->wx_appId);
  121. // break;
  122. // case self::PAY_TYPE_MINI_PROGRAM:
  123. // $this->appId = trim($wxConfig->mini_program_appId);
  124. // break;
  125. // }
  126. $this->mchId = $wxPayConfig->mch_id;
  127. $this->certPath = trim($path . $wxPayConfig->cert_path);
  128. $this->keyPath = trim($path . $wxPayConfig->key_path);
  129. $this->notifyUrl = Yii::$app->request->hostInfo . '/wx-payment/notify';
  130. }
  131. /**
  132. * @param $unifyParams
  133. * @return mixed
  134. * 统一下单
  135. */
  136. private function unify($unifyParams)
  137. {
  138. $this->getPaymentApp();
  139. return $this->app->order->unify($unifyParams);
  140. }
  141. /**
  142. * @return array|bool
  143. * @throws BadRequestHttpException
  144. * @throws \yii\base\InvalidConfigException
  145. * @throws \yii\httpclient\Exception
  146. * 微信支付回调
  147. */
  148. public function notify()
  149. {
  150. $notifyData = Json::decode(Json::encode(simplexml_load_string(Yii::$app->request->getRawBody(), 'SimpleXMLElement', LIBXML_NOCDATA)));
  151. Yii::info($notifyData, "notify");
  152. if (!$this->checkSign($notifyData)) {
  153. throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
  154. }
  155. $tra = Yii::$app->db->beginTransaction('SERIALIZABLE');
  156. try {
  157. if ($notifyData->result_code != 'SUCCESS' || $notifyData->return_code != 'SUCCESS') {
  158. throw new BadRequestHttpException('result_code or return_code is false');
  159. }
  160. $paymentLog = PaymentLog::findOne(['order_id' => $notifyData->out_trade_no]);
  161. $this->notifyUrl = Yii::$app->request->hostInfo . $paymentLog->notify_url;
  162. $paymentLog->mch_id = $notifyData->mch_id;
  163. $paymentLog->wx_refund_id = $notifyData->transaction_id; //交易号
  164. $paymentLog->status = PaymentLog::PAYMENT_STATUS_SUCCESS;
  165. if (!$paymentLog->save()) {
  166. throw new Exception(Helper::errorMessageStr($paymentLog->errors));
  167. }
  168. if (!$tra->commit()) {
  169. throw new Exception('保存数据失败');
  170. }
  171. /*转发回调信息*/
  172. $this->forwardNotify($notifyData, true);
  173. return ['return_code' => 'SUCCESS', 'return_msg' => 'OK'];//回传成功信息到微信服务器
  174. } catch (Exception $e) {
  175. $tra->rollBack();
  176. $this->forwardNotify($notifyData, false);
  177. Yii::info($e->getMessage(), 'notify');
  178. return false;
  179. } catch (BadRequestHttpException $e) {
  180. Yii::info($e->getMessage(), 'notify');
  181. return false;
  182. }
  183. }
  184. /**
  185. * @param $notifyData
  186. * @param $status
  187. * @return bool
  188. * @throws \yii\base\InvalidConfigException
  189. * @throws \yii\httpclient\Exception
  190. * 转发异步回调信息
  191. */
  192. private function forwardNotify($notifyData, $status)
  193. {
  194. $notify = [
  195. 'notify' => [
  196. 'status' => $status,
  197. 'notify' => $notifyData
  198. ]
  199. ];
  200. $client = new Client();
  201. $response = $client->createRequest()
  202. ->setMethod('POST')
  203. ->setUrl($this->notifyUrl)
  204. ->addHeaders(['content-type' => 'application/json'])
  205. ->setContent(Json::encode($notify))
  206. ->send();
  207. if ($response->isOk) {
  208. return true;
  209. } else {
  210. return false;
  211. }
  212. }
  213. /**
  214. * @param $data
  215. * @return bool
  216. * 支付成功回调验证签名和支付金额
  217. */
  218. public function checkSign($data)
  219. {
  220. $this->initObject();
  221. $notifySign = $data['sign'];
  222. unset($data['sign']);
  223. $sign = $this->_sign($data);
  224. if ($notifySign == $sign) {
  225. return true;
  226. } else {
  227. return false;
  228. }
  229. }
  230. /**
  231. * @param $arr
  232. * @return string
  233. * 微信签名方法
  234. */
  235. private function _sign($arr)
  236. {
  237. $arr = array_filter($arr);
  238. ksort($arr);
  239. $arr['key'] = $this->key;
  240. $queryString = http_build_query($arr);
  241. $queryString = urldecode($queryString);
  242. return strtoupper(md5($queryString));
  243. }
  244. }