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.

357 lines
12 KiB

  1. <?php
  2. namespace backend\modules\payment\logic;
  3. use api\logic\Helper;
  4. use backend\modules\shop\models\ars\PaymentLog;
  5. use backend\modules\shop\models\ars\RefundLog;
  6. use backend\modules\shop\models\ars\WxPayConfig;
  7. use EasyWeChat\Factory;
  8. use yii\db\Exception;
  9. use yii\helpers\Json;
  10. use yii\web\BadRequestHttpException;
  11. use yii\web\NotFoundHttpException;
  12. use Yii;
  13. class WxPaymentManager
  14. {
  15. /*支付类型*/
  16. const PAY_TYPE_WEB = 1;
  17. const PAY_TYPE_MINI_PROGRAM = 2;
  18. /*支付状态*/
  19. const STATUS_PAYMENT_WAITING = 0;
  20. const STATUS_PAYMENT_SUCCESS = 1;
  21. /*退款状态*/
  22. const STATUS_REFUND_WAIT = 0; //退款待审核
  23. const STATUS_REFUND_CONFIRM = 1; //退款待确认
  24. const STATUS_REFUND_SUCCESS = 2; //退款成功
  25. const STATUS_REFUND_PORTION = 3; //部分退款
  26. /*发起支付方式*/
  27. const TRADE_TYPE_JS_API = 'JSAPI';
  28. public $appId;
  29. public $mchId;
  30. public $key;
  31. public $certPath;
  32. public $keyPath;
  33. public $notifyUrl;
  34. public $tradeType;
  35. public $payType;
  36. public $app;
  37. public $order;
  38. /**
  39. * @param $orderId
  40. * @param $orderAmount
  41. * @param $paymentAmount
  42. * @param $notifyUrl
  43. * @param $payType
  44. * @return mixed
  45. * @throws BadRequestHttpException
  46. * @throws Exception
  47. * 微信统一下单
  48. */
  49. public function wxPayment($orderId, $orderAmount, $paymentAmount, $notifyUrl, $payType)
  50. {
  51. if (empty($orderId) || empty($orderAmount) ||empty($paymentAmount) || empty($notifyUrl) || empty($payType)) {
  52. throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
  53. }
  54. $this->tradeType = self::TRADE_TYPE_JS_API;
  55. $this->payType = $payType;
  56. $this->notifyUrl = $notifyUrl;
  57. $unifyParams = $this->applyPaymentData($orderId, $orderAmount, $paymentAmount, $notifyUrl);
  58. $result = $this->unify($unifyParams);
  59. if ($result['return_code'] == 'FAIL') {
  60. throw new BadRequestHttpException($result['return_msg']);
  61. }
  62. if ($result['return_code'] == 'SUCCESS' && $result['return_msg'] == 'OK' && $result['result_code'] == 'FAIL') {
  63. throw new BadRequestHttpException($result['err_code_des']);
  64. }
  65. return Json::decode($this->getBridgeConfig($result['prepay_id']), true);
  66. }
  67. /**
  68. * @param $prepayId
  69. * @return array|string
  70. */
  71. private function getBridgeConfig($prepayId)
  72. {
  73. $this->initObject();
  74. return $this->app->jssdk->bridgeConfig($prepayId);
  75. }
  76. /**
  77. * @param string $orderId
  78. * @param int $orderAmount
  79. * @param int $paymentAmount
  80. * @param string $notifyUrl
  81. * @return array
  82. * @throws BadRequestHttpException
  83. * @throws Exception
  84. * 生成支付参数
  85. */
  86. private function applyPaymentData($orderId, $orderAmount, $paymentAmount, $notifyUrl)
  87. {
  88. $this->savePaymentLog($orderId, $orderAmount, $paymentAmount, $notifyUrl);
  89. $params = [
  90. 'body' => '订单支付',
  91. 'out_trade_no' => $orderId,
  92. 'total_fee' => $paymentAmount,
  93. 'openid' => Yii::$app->user->identity->wx_openid,
  94. ];
  95. return $params;
  96. }
  97. /**
  98. * @param string $orderId
  99. * @param int $orderAmount
  100. * @param int $paymentAmount
  101. * @param string $notifyUrl
  102. * @throws BadRequestHttpException
  103. * @throws Exception
  104. * 保存支付信息
  105. */
  106. private function savePaymentLog($orderId, $orderAmount, $paymentAmount, $notifyUrl)
  107. {
  108. $paymentLog = PaymentLog::findOne(['order_id' => $orderId]);
  109. if (!$paymentLog) {
  110. $paymentLog = new PaymentLog();
  111. } elseif (!empty($paymentLog->wx_payment_id)) {
  112. throw new BadRequestHttpException('此订单号也有支付信息,不能重复');
  113. }
  114. $paymentLog->order_id = $orderId;
  115. $paymentLog->order_amount = $orderAmount;
  116. $paymentLog->payment_amount = $paymentAmount;
  117. $paymentLog->notify_url = $this->notifyUrl;
  118. $paymentLog->type = $this->payType;
  119. $paymentLog->status = self::STATUS_PAYMENT_WAITING;
  120. if (!$paymentLog->save()) {
  121. throw new Exception(Helper::errorMessageStr($paymentLog->errors));
  122. }
  123. }
  124. protected function getApp()
  125. {
  126. $this->initObject();
  127. $config = [
  128. 'app_id' => $this->appId,
  129. 'mch_id' => $this->mchId,
  130. 'key' => $this->key,
  131. 'cert_path' => $this->certPath,
  132. 'key_path' => $this->keyPath,
  133. 'notify_url' => $this->notifyUrl,
  134. 'trade_type' => $this->tradeType,
  135. 'sandbox' => true, // 设置为 false 或注释则关闭沙箱模式
  136. ];
  137. $this->app = Factory::payment($config);
  138. //判断当前是否为沙箱模式:
  139. $this->app->inSandbox();
  140. }
  141. /**
  142. * @var WxPayConfig $wxPayConfig
  143. */
  144. private function initObject()
  145. {
  146. $path = Yii::getAlias('@backend');
  147. $wxPayConfig = WxPayConfig::find()->one();
  148. switch ($this->payType) {
  149. case self::PAY_TYPE_WEB:
  150. // $wxConfig = WxConfig::find()->one();
  151. // $this->appId = trim($wxConfig->appid);
  152. break;
  153. case self::PAY_TYPE_MINI_PROGRAM:
  154. // $miniProgramConfig = MiniProgramConfig::find()->one();
  155. // $this->appId = trim($miniProgramConfig->appid);
  156. break;
  157. }
  158. $this->mchId = trim($wxPayConfig->mch_id);
  159. $this->key = trim($wxPayConfig->key);
  160. $this->certPath = trim($path . $wxPayConfig->cert_path);
  161. $this->keyPath = trim($path . $wxPayConfig->key_path);
  162. $this->notifyUrl = Yii::$app->request->hostInfo . '/wx-payment/notify';
  163. $this->mchId = '1395812402';
  164. $this->key = '51CF5EE3B2E35B9843E17E6099325A65';
  165. }
  166. /**
  167. * @param $unifyParams
  168. * @return mixed
  169. * 统一下单
  170. */
  171. private function unify($unifyParams)
  172. {
  173. $unifyParams['trade_type'] = $this->tradeType;
  174. $this->getApp();
  175. return $this->app->order->unify($unifyParams);
  176. }
  177. /**
  178. * @param $data
  179. * @return bool
  180. * 支付成功回调验证签名和支付金额
  181. */
  182. public function checkSign($data)
  183. {
  184. $this->initObject();
  185. $notifySign = $data['sign'];
  186. unset($data['sign']);
  187. $sign = $this->_sign($data);
  188. if ($notifySign == $sign) {
  189. return true;
  190. } else {
  191. return false;
  192. }
  193. }
  194. /**
  195. * @param $arr
  196. * @return string
  197. * 微信签名方法
  198. */
  199. private function _sign($arr)
  200. {
  201. $arr = array_filter($arr);
  202. ksort($arr);
  203. $arr['key'] = $this->key;
  204. $queryString = http_build_query($arr);
  205. $queryString = urldecode($queryString);
  206. return strtoupper(md5($queryString));
  207. }
  208. /*************************************** Refund ************************************************/
  209. /**
  210. * @param string $orderId
  211. * @param int $refundAmount
  212. * @param string $refundAccount
  213. * @param int $reason
  214. * @return RefundLog
  215. * @throws BadRequestHttpException
  216. * @throws Exception
  217. * @throws NotFoundHttpException
  218. * 申请退款
  219. */
  220. public function applyRefund($orderId, $refundAmount, $refundAccount, $reason)
  221. {
  222. if (empty($orderId) || empty($refundAmount) || empty($refundAccount) || empty($reason)) {
  223. throw new BadRequestHttpException(Helper::REQUEST_BAD_PARAMS);
  224. }
  225. $paymentLog = PaymentLog::findOne(['order_id' => $orderId]);
  226. if (empty($paymentLog)) {
  227. throw new NotFoundHttpException('订单支付信息未找到');
  228. }
  229. if (RefundLog::findOne(['order_id' => $orderId, 'status' => self::STATUS_REFUND_WAIT])) {
  230. throw new BadRequestHttpException('此订单存在等待审核的退款申请');
  231. }
  232. $refundedAmount = RefundLog::find()
  233. ->where(['order_id' => $orderId, 'status' => self::STATUS_PAYMENT_SUCCESS])
  234. ->sum('refund_amount') ?? 0;
  235. $refundLog = new RefundLog();
  236. $refundLog->order_id = $orderId;
  237. $refundLog->wx_refund_id = Helper::timeRandomNum(3, 'R');
  238. $refundLog->reason = $reason;
  239. $refundLog->order_amount = $paymentLog->payment_amount;
  240. $refundLog->refund_amount = $refundAmount;
  241. $refundLog->refunded_amount = $refundedAmount;
  242. $refundLog->type = $paymentLog->type;
  243. $refundLog->status = self::STATUS_REFUND_WAIT;
  244. $refundLog->refund_account = $refundAccount;
  245. $refundLog->applyed_at = time();
  246. if (!$refundLog->save()) {
  247. throw new Exception(Helper::errorMessageStr($refundLog->errors));
  248. }
  249. return $refundLog;
  250. }
  251. /**
  252. * @param string $orderId
  253. * @param int $refundAmount
  254. * @param int $operatorId
  255. * @return array|mixed
  256. * @throws BadRequestHttpException
  257. * @throws NotFoundHttpException
  258. */
  259. public function refund($orderId, $refundAmount, $operatorId)
  260. {
  261. $paymentLog = PaymentLog::findOne(['order_id' => $orderId]);
  262. if (empty($paymentLog)) {
  263. throw new NotFoundHttpException('订单支付信息未找到');
  264. }
  265. $refundLog = RefundLog::findOne(['order_id' => $orderId, 'status' => self::STATUS_REFUND_WAIT]);
  266. if (empty($refundLog)) {
  267. throw new NotFoundHttpException('订单退款信息未找到');
  268. }
  269. $this->executeRefund($paymentLog, $refundLog, $refundAmount);
  270. return $this->updateRefundInfo($paymentLog, $refundLog, $operatorId);
  271. }
  272. /**
  273. * @param PaymentLog $paymentLog
  274. * @param RefundLog $refundLog
  275. * @param $refundAmount
  276. * @throws BadRequestHttpException
  277. */
  278. private function executeRefund($paymentLog, $refundLog, $refundAmount)
  279. {
  280. /*参数分别为:微信订单号、商户退款单号、订单金额、退款金额、其他参数*/
  281. $this->getApp();
  282. $config = [
  283. 'refund_desc' => '退款'
  284. ];
  285. $result = $this->app->refund->byTransactionId(
  286. $paymentLog->wx_payment_id,
  287. $refundLog->wx_refund_id,
  288. $paymentLog->payment_amount,
  289. $refundAmount,
  290. $config
  291. );
  292. Yii::info($result, 'refund_log');
  293. if ($result['return_code'] == 'FAIL' || $result['return_msg'] != 'OK' || $result['result_code'] == 'FAIL') {
  294. throw new BadRequestHttpException($result['return_msg']);
  295. }
  296. if ($result['err_code_des']) {
  297. throw new BadRequestHttpException($result['err_code_des']);
  298. }
  299. }
  300. /**
  301. * @param PaymentLog $paymentLog
  302. * @param RefundLog $refundLog
  303. * @param int $operatorId
  304. * @return array
  305. */
  306. private function updateRefundInfo($paymentLog, $refundLog, $operatorId)
  307. {
  308. $tra = Yii::$app->db->beginTransaction();
  309. try {
  310. $paymentLog->wx_refund_id = $refundLog->wx_refund_id;
  311. $paymentLog->refund_account = $refundLog->refund_account;
  312. $paymentLog->status = $refundLog->refund_amount < $paymentLog->payment_amount ? self::STATUS_REFUND_SUCCESS : self::STATUS_REFUND_PORTION;
  313. if (!$paymentLog->save()) {
  314. throw new Exception(Helper::errorMessageStr($paymentLog->errors));
  315. }
  316. $refundLog->operator_id = $operatorId;
  317. $refundLog->status = $refundLog->refund_amount < $paymentLog->payment_amount ? self::STATUS_REFUND_SUCCESS : self::STATUS_REFUND_PORTION;
  318. $refundLog->finished_at = time();
  319. if (!$refundLog->save()) {
  320. throw new Exception(Helper::errorMessageStr($refundLog->errors));
  321. }
  322. $tra->commit();
  323. return ['status' => true];
  324. } catch (Exception $e) {
  325. $tra->rollBack();
  326. return ['status' => false, 'info' => $e->getMessage()];
  327. }
  328. }
  329. }