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.

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