Service.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <?php
  2. namespace addons\epay\library;
  3. use addons\third\model\Third;
  4. use app\common\library\Auth;
  5. use Exception;
  6. use think\Hook;
  7. use think\Session;
  8. use Yansongda\Pay\Pay;
  9. use Yansongda\Supports\Str;
  10. /**
  11. * 订单服务类
  12. *
  13. * @package addons\epay\library
  14. */
  15. class Service
  16. {
  17. public const SDK_VERSION_V2 = 'v2';
  18. public const SDK_VERSION_V3 = 'v3';
  19. /**
  20. * 提交退款订单
  21. * @param array|float $amount 订单金额
  22. * @param array|float $refund_money 退款金额
  23. * @param string $orderid 订单号
  24. * @param string $refund_sn 退款订单号
  25. * @param string $type 支付类型,可选alipay或wechat
  26. * @param string $remark 退款原因
  27. * @param string $notifyurl 通知回调URL
  28. * @param string $returnurl 跳转返回URL
  29. * @param string $method 支付方式
  30. * @return Response|RedirectResponse|Collection
  31. * @throws Exception
  32. */
  33. public static function submitRefund($amount=null,$refund_money,$orderid,$refund_sn,$type,$remark = null,$notifyurl = null,$returnurl = null,$method = 'app'){
  34. if (!is_array($amount)) {
  35. $params = [
  36. 'amount' => $amount,
  37. 'type' => $type,
  38. 'notifyurl' => $notifyurl,
  39. 'returnurl' => $returnurl,
  40. 'method' => $method,
  41. ];
  42. } else {
  43. $params = $amount;
  44. }
  45. $type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat';
  46. $request = request();
  47. $notifyurl = isset($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'notify';
  48. // $returnurl = isset($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'return/out_trade_no/' . $orderid;
  49. $config = Service::getConfig($type);
  50. $config['notify_url'] = $notifyurl;
  51. $config['return_url'] = $returnurl;
  52. $result = null;
  53. //退款参数
  54. $order_data = [
  55. 'out_trade_no' => $orderid//原订单号
  56. ];
  57. if ($type == 'wechat') {
  58. //创建支付对象
  59. $pay = Pay::wechat($config);
  60. $total_fee = $amount * 100;
  61. $refund_fee = $refund_money * 100;
  62. $order_data = array_merge($order_data, [
  63. 'out_refund_no' => $refund_sn,//退款订单号
  64. 'total_fee' => $total_fee,//支付金额
  65. 'refund_fee' => $refund_fee,//退款金额
  66. 'refund_desc' => $remark,//退款原因
  67. //'type' => $method //支付方式
  68. ]);
  69. } else {
  70. $pay = Pay::alipay($config);
  71. $order_data = array_merge($order_data, [
  72. 'out_request_no' => $refund_sn,//退款订单号
  73. 'refund_amount' => $refund_money,
  74. ]);
  75. }
  76. $result = $pay->refund($order_data);
  77. //使用重写的Response类、RedirectResponse、Collection类
  78. if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
  79. $result = new RedirectResponse($result->getTargetUrl());
  80. } elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) {
  81. $result = new Response($result->getContent());
  82. } elseif ($result instanceof \Yansongda\Supports\Collection) {
  83. $result = Collection::make($result->all());
  84. } elseif ($result instanceof \GuzzleHttp\Psr7\Response) {
  85. $result = new Response($result->getBody());
  86. }
  87. return $result;
  88. }
  89. /**
  90. * 提交订单
  91. * @param array|float $amount 订单金额
  92. * @param string $orderid 订单号
  93. * @param string $type 支付类型,可选alipay或wechat
  94. * @param string $title 订单标题
  95. * @param string $notifyurl 通知回调URL
  96. * @param string $returnurl 跳转返回URL
  97. * @param string $method 支付方法
  98. * @param string $openid Openid
  99. * @param array $custom 自定义微信支付宝相关配置
  100. * @return Response|RedirectResponse|Collection
  101. * @throws Exception
  102. */
  103. public static function submitOrder($amount, $orderid = null, $type = null, $title = null, $notifyurl = null, $returnurl = null, $method = null, $openid = '', $custom = [])
  104. {
  105. $version = self::getSdkVersion();
  106. $request = request();
  107. $addonConfig = get_addon_config('epay');
  108. if (!is_array($amount)) {
  109. $params = [
  110. 'amount' => $amount,
  111. 'orderid' => $orderid,
  112. 'type' => $type,
  113. 'title' => $title,
  114. 'notifyurl' => $notifyurl,
  115. 'returnurl' => $returnurl,
  116. 'method' => $method,
  117. 'openid' => $openid,
  118. 'custom' => $custom,
  119. ];
  120. } else {
  121. $params = $amount;
  122. }
  123. $type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat';
  124. $method = $params['method'] ?? 'web';
  125. $orderid = $params['orderid'] ?? date("YmdHis") . mt_rand(100000, 999999);
  126. $amount = $params['amount'] ?? 1;
  127. $title = $params['title'] ?? "支付";
  128. $auth_code = $params['auth_code'] ?? '';
  129. $openid = $params['openid'] ?? '';
  130. //自定义微信支付宝相关配置
  131. $custom = $params['custom'] ?? [];
  132. //未定义则使用默认回调和跳转
  133. $notifyurl = !empty($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type;
  134. $returnurl = !empty($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $orderid;
  135. $html = '';
  136. $config = Service::getConfig($type, array_merge($custom, ['notify_url' => $notifyurl, 'return_url' => $returnurl]));
  137. //判断是否移动端或微信内浏览器
  138. $isMobile = $request->isMobile();
  139. $isWechat = strpos($request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
  140. $result = null;
  141. if ($type == 'alipay') {
  142. //如果是PC支付,判断当前环境,进行跳转
  143. if ($method == 'web') {
  144. //如果是微信环境或后台配置PC使用扫码支付
  145. if ($isWechat || $addonConfig['alipay']['scanpay']) {
  146. Session::set("alipayorderdata", $params);
  147. $url = addon_url('epay/api/alipay', [], true, true);
  148. return new RedirectResponse($url);
  149. } elseif ($isMobile) {
  150. $method = 'wap';
  151. }
  152. }
  153. //创建支付对象
  154. $pay = Pay::alipay($config);
  155. $params = [
  156. 'out_trade_no' => $orderid,//你的订单号
  157. 'total_amount' => $amount,//单位元
  158. 'subject' => $title,
  159. ];
  160. switch ($method) {
  161. case 'web':
  162. //电脑支付
  163. $result = $pay->web($params);
  164. break;
  165. case 'wap':
  166. //手机网页支付
  167. $result = $pay->wap($params);
  168. break;
  169. case 'app':
  170. //APP支付
  171. $result = $pay->app($params);
  172. break;
  173. case 'scan':
  174. //扫码支付
  175. $result = $pay->scan($params);
  176. break;
  177. case 'pos':
  178. //刷卡支付必须要有auth_code
  179. $params['auth_code'] = $auth_code;
  180. $result = $pay->pos($params);
  181. break;
  182. case 'mini':
  183. case 'miniapp':
  184. //小程序支付
  185. //小程序支付,直接返回字符串
  186. //小程序支付必须要有buyer_id,这里使用openid
  187. $params['buyer_id'] = $openid;
  188. $result = $pay->mini($params);
  189. break;
  190. default:
  191. }
  192. } else {
  193. //如果是PC支付,判断当前环境,进行跳转
  194. if ($method == 'web') {
  195. //如果是移动端,但不是微信环境
  196. if ($isMobile && !$isWechat) {
  197. $method = 'wap';
  198. } else {
  199. Session::set("wechatorderdata", $params);
  200. $url = addon_url('epay/api/wechat', [], true, true);
  201. return new RedirectResponse($url);
  202. }
  203. }
  204. //单位分
  205. $total_fee = function_exists('bcmul') ? bcmul($amount, 100) : $amount * 100;
  206. $total_fee = (int)$total_fee;
  207. $ip = $request->ip();
  208. //微信服务商模式时需传递sub_openid参数
  209. $openidName = $addonConfig['wechat']['mode'] == 'service' ? 'sub_openid' : 'openid';
  210. //创建支付对象
  211. $pay = Pay::wechat($config);
  212. if (self::isVersionV3()) {
  213. //V3支付
  214. $params = [
  215. 'out_trade_no' => $orderid,
  216. 'description' => $title,
  217. 'amount' => [
  218. 'total' => $total_fee,
  219. ]
  220. ];
  221. switch ($method) {
  222. case 'mp':
  223. //公众号支付
  224. //公众号支付必须有openid
  225. $params['payer'] = [$openidName => $openid];
  226. $result = $pay->mp($params);
  227. break;
  228. case 'wap':
  229. //手机网页支付,跳转
  230. $params['scene_info'] = [
  231. 'payer_client_ip' => $ip,
  232. 'h5_info' => [
  233. 'type' => 'Wap',
  234. ]
  235. ];
  236. $result = $pay->wap($params);
  237. break;
  238. case 'app':
  239. //APP支付,直接返回字符串
  240. $result = $pay->app($params);
  241. break;
  242. case 'scan':
  243. //扫码支付,直接返回字符串
  244. $result = $pay->scan($params);
  245. break;
  246. case 'pos':
  247. //刷卡支付,直接返回字符串
  248. //刷卡支付必须要有auth_code
  249. $params['auth_code'] = $auth_code;
  250. $result = $pay->pos($params);
  251. break;
  252. case 'mini':
  253. case 'miniapp':
  254. //小程序支付,直接返回字符串
  255. //小程序支付必须要有openid
  256. $params['payer'] = [$openidName => $openid];
  257. $result = $pay->mini($params);
  258. break;
  259. default:
  260. }
  261. } else {
  262. //V2支付
  263. $params = [
  264. 'out_trade_no' => $orderid,
  265. 'body' => $title,
  266. 'total_fee' => $total_fee,
  267. ];
  268. switch ($method) {
  269. case 'mp':
  270. //公众号支付
  271. //公众号支付必须有openid
  272. $params[$openidName] = $openid;
  273. $result = $pay->mp($params);
  274. break;
  275. case 'wap':
  276. //手机网页支付,跳转
  277. $params['spbill_create_ip'] = $ip;
  278. $result = $pay->wap($params);
  279. break;
  280. case 'app':
  281. //APP支付,直接返回字符串
  282. $result = $pay->app($params);
  283. break;
  284. case 'scan':
  285. //扫码支付,直接返回字符串
  286. $result = $pay->scan($params);
  287. break;
  288. case 'pos':
  289. //刷卡支付,直接返回字符串
  290. //刷卡支付必须要有auth_code
  291. $params['auth_code'] = $auth_code;
  292. $result = $pay->pos($params);
  293. break;
  294. case 'mini':
  295. case 'miniapp':
  296. //小程序支付,直接返回字符串
  297. //小程序支付必须要有openid
  298. $params[$openidName] = $openid;
  299. $result = $pay->miniapp($params);
  300. break;
  301. default:
  302. }
  303. }
  304. }
  305. //使用重写的Response类、RedirectResponse、Collection类
  306. if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
  307. $result = new RedirectResponse($result->getTargetUrl());
  308. } elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) {
  309. $result = new Response($result->getContent());
  310. } elseif ($result instanceof \Yansongda\Supports\Collection) {
  311. $result = Collection::make($result->all());
  312. } elseif ($result instanceof \GuzzleHttp\Psr7\Response) {
  313. $result = new Response($result->getBody());
  314. }
  315. return $result;
  316. }
  317. /**
  318. * 验证回调是否成功
  319. * @param string $type 支付类型
  320. * @param array $custom 自定义配置信息
  321. * @return bool|\Yansongda\Pay\Gateways\Alipay|\Yansongda\Pay\Gateways\Wechat|\Yansongda\Pay\Provider\Wechat|\Yansongda\Pay\Provider\Alipay
  322. */
  323. public static function checkNotify($type, $custom = [])
  324. {
  325. $type = strtolower($type);
  326. if (!in_array($type, ['wechat', 'alipay'])) {
  327. return false;
  328. }
  329. $version = self::getSdkVersion();
  330. try {
  331. $config = self::getConfig($type, $custom);
  332. $pay = $type == 'wechat' ? Pay::wechat($config) : Pay::alipay($config);
  333. $data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
  334. if ($type == 'alipay') {
  335. if (in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
  336. return $pay;
  337. }
  338. } else {
  339. return $pay;
  340. }
  341. } catch (Exception $e) {
  342. \think\Log::record("回调请求参数解析错误", "error");
  343. return false;
  344. }
  345. return false;
  346. }
  347. /**
  348. * 验证返回是否成功,请勿用于判断是否支付成功的逻辑验证
  349. * 已弃用
  350. *
  351. * @param string $type 支付类型
  352. * @param array $custom 自定义配置信息
  353. * @return bool
  354. * @deprecated 已弃用,请勿用于逻辑验证
  355. */
  356. public static function checkReturn($type, $custom = [])
  357. {
  358. //由于PC及移动端无法获取请求的参数信息,取消return验证,均返回true
  359. return true;
  360. }
  361. /**
  362. * 获取配置
  363. * @param string $type 支付类型
  364. * @param array $custom 自定义配置,用于覆盖插件默认配置
  365. * @return array
  366. */
  367. public static function getConfig($type = 'wechat', $custom = [])
  368. {
  369. $addonConfig = get_addon_config('epay');
  370. $config = $addonConfig[$type] ?? $addonConfig['wechat'];
  371. // SDK版本
  372. $version = self::getSdkVersion();
  373. if (isset($config['cert_client']) && substr($config['cert_client'], 0, 8) == '/addons/') {
  374. $config['cert_client'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_client'], 1));
  375. }
  376. if (isset($config['cert_key']) && substr($config['cert_key'], 0, 8) == '/addons/') {
  377. $config['cert_key'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_key'], 1));
  378. }
  379. if (isset($config['app_cert_public_key']) && substr($config['app_cert_public_key'], 0, 8) == '/addons/') {
  380. $config['app_cert_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['app_cert_public_key'], 1));
  381. }
  382. if (isset($config['alipay_root_cert']) && substr($config['alipay_root_cert'], 0, 8) == '/addons/') {
  383. $config['alipay_root_cert'] = ROOT_PATH . str_replace('/', DS, substr($config['alipay_root_cert'], 1));
  384. }
  385. if (isset($config['ali_public_key']) && (Str::endsWith($config['ali_public_key'], '.crt') || Str::endsWith($config['ali_public_key'], '.pem'))) {
  386. $config['ali_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['ali_public_key'], 1));
  387. }
  388. // V3支付
  389. if (self::isVersionV3()) {
  390. if ($type == 'wechat') {
  391. $config['mp_app_id'] = $config['app_id'] ?? '';
  392. $config['app_id'] = $config['appid'] ?? '';
  393. $config['mini_app_id'] = $config['miniapp_id'] ?? '';
  394. $config['combine_mch_id'] = $config['combine_mch_id'] ?? '';
  395. $config['mch_secret_key'] = $config['key_v3'] ?? '';
  396. $config['mch_secret_cert'] = $config['cert_key'];
  397. $config['mch_public_cert_path'] = $config['cert_client'];
  398. $config['sub_mp_app_id'] = $config['sub_appid'] ?? '';
  399. $config['sub_app_id'] = $config['sub_app_id'] ?? '';
  400. $config['sub_mini_app_id'] = $config['sub_miniapp_id'] ?? '';
  401. $config['sub_mch_id'] = $config['sub_mch_id'] ?? '';
  402. } elseif ($type == 'alipay') {
  403. $config['app_secret_cert'] = $config['private_key'] ?? '';
  404. $config['app_public_cert_path'] = $config['app_cert_public_key'] ?? '';
  405. $config['alipay_public_cert_path'] = $config['ali_public_key'] ?? '';
  406. $config['alipay_root_cert_path'] = $config['alipay_root_cert'] ?? '';
  407. $config['service_provider_id'] = $config['pid'] ?? '';
  408. }
  409. $modeArr = ['normal' => 0, 'dev' => 1, 'service' => 2];
  410. $config['mode'] = $modeArr[$config['mode']] ?? 0;
  411. }
  412. // 日志
  413. if ($config['log']) {
  414. $config['log'] = [
  415. 'enable' => true,
  416. 'file' => LOG_PATH . 'epaylogs' . DS . $type . '-' . date("Y-m-d") . '.log',
  417. 'level' => 'debug'
  418. ];
  419. } else {
  420. $config['log'] = [
  421. 'enable' => false,
  422. ];
  423. }
  424. // GuzzleHttp配置,可选
  425. $config['http'] = [
  426. 'timeout' => 10,
  427. 'connect_timeout' => 10,
  428. // 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
  429. ];
  430. $config['notify_url'] = empty($config['notify_url']) ? addon_url('epay/api/notifyx', [], false) . '/type/' . $type : $config['notify_url'];
  431. $config['notify_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['notify_url']) ? request()->root(true) . $config['notify_url'] : $config['notify_url'];
  432. $config['return_url'] = empty($config['return_url']) ? addon_url('epay/api/returnx', [], false) . '/type/' . $type : $config['return_url'];
  433. $config['return_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['return_url']) ? request()->root(true) . $config['return_url'] : $config['return_url'];
  434. //合并自定义配置
  435. $config = array_merge($config, $custom);
  436. //v3版本时返回的结构不同
  437. if (self::isVersionV3()) {
  438. $config = [$type => ['default' => $config], 'logger' => $config['log'], 'http' => $config['http'], '_force' => true];
  439. }
  440. return $config;
  441. }
  442. /**
  443. * 获取微信Openid
  444. *
  445. * @param array $custom 自定义配置信息
  446. * @return mixed|string
  447. */
  448. public static function getOpenid($custom = [])
  449. {
  450. $openid = '';
  451. $auth = Auth::instance();
  452. if ($auth->isLogin()) {
  453. $third = get_addon_info('third');
  454. if ($third && $third['state']) {
  455. $thirdInfo = Third::where('user_id', $auth->id)->where('platform', 'wechat')->where('apptype', 'mp')->find();
  456. $openid = $thirdInfo ? $thirdInfo['openid'] : '';
  457. }
  458. }
  459. if (!$openid) {
  460. $openid = Session::get("openid");
  461. //如果未传openid,则去读取openid
  462. if (!$openid) {
  463. $addonConfig = get_addon_config('epay');
  464. $wechat = new Wechat($custom['app_id'] ?? $addonConfig['wechat']['app_id'], $custom['app_secret'] ?? $addonConfig['wechat']['app_secret']);
  465. $openid = $wechat->getOpenid();
  466. }
  467. }
  468. return $openid;
  469. }
  470. /**
  471. * 获取SDK版本
  472. * @return mixed|string
  473. */
  474. public static function getSdkVersion()
  475. {
  476. $addonConfig = get_addon_config('epay');
  477. return $addonConfig['version'] ?? self::SDK_VERSION_V2;
  478. }
  479. /**
  480. * 判断是否V2支付
  481. * @return bool
  482. */
  483. public static function isVersionV2()
  484. {
  485. return self::getSdkVersion() === self::SDK_VERSION_V2;
  486. }
  487. /**
  488. * 判断是否V3支付
  489. * @return bool
  490. */
  491. public static function isVersionV3()
  492. {
  493. return self::getSdkVersion() === self::SDK_VERSION_V3;
  494. }
  495. }