Service.php 18 KB

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