WechatService.php 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468
  1. <?php
  2. namespace App\Services;
  3. use App\Models\FansModel;
  4. use App\Models\MemberModel;
  5. use BaconQrCode\Renderer\Image\ImagickImageBackEnd;
  6. use BaconQrCode\Renderer\ImageRenderer;
  7. use BaconQrCode\Renderer\RendererStyle\RendererStyle;
  8. use BaconQrCode\Writer;
  9. use phpDocumentor\Reflection\Types\Self_;
  10. use Symfony\Component\Console\Input\Input;
  11. use WeChatPay\Builder;
  12. use WeChatPay\Util\PemUtil;
  13. class WechatService extends BaseService
  14. {
  15. private static $apiUrl = [
  16. // 授权
  17. 'auth' => 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=xydc#besi_redirect',
  18. // 第三方
  19. 'qrConnect' => 'https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=SCOPE&state=STATE',
  20. // 永久ACCESS_TOKEN
  21. 'accessToken' => 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s',
  22. // 临时ACCESS_TOKEN
  23. 'tempToken' => 'https://api.weixin.qq.com/sns/oauth2/access_token?code=%s&appid=%s&secret=%s&grant_type=authorization_code',
  24. // 微信用户信息
  25. 'wxInfo' => 'https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN',
  26. // 获取userInfo
  27. 'userInfo' => 'https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN',
  28. // 统一下单V3
  29. 'unifiedorderV3' => 'https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi',
  30. // 统一下单V2
  31. 'unifiedorder' => 'https://api.mch.weixin.qq.com/pay/unifiedorder',
  32. // 原路退款接口
  33. 'refundOrder' => 'https://api.mch.weixin.qq.com/pay/unifiedorder',
  34. // 查询订单
  35. 'queryOrder' => 'https://api.mch.weixin.qq.com/pay/orderquery',
  36. // 企业付款到零钱
  37. 'transfers' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers',
  38. // 查询企业付款订单
  39. 'queryTransfer' => 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo',
  40. // 生成二维码
  41. 'makeQrcode' => 'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s',
  42. // 换取二维码
  43. 'getQrcodeByTicket' => 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s',
  44. // 创建公众号菜单
  45. 'createMenu' => 'https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s',
  46. // 获取公众号菜单
  47. 'getMenu' => 'https://api.weixin.qq.com/cgi-bin/menu/get?access_token=%s',
  48. // 删除公众号菜单
  49. 'delMenu' => 'https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=%s',
  50. // 发送客服消息
  51. 'customMessage' => 'https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=%s',
  52. // 发送模板消息
  53. 'tplMessage' => 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s',
  54. // 获取消息模板列表
  55. 'templateList' => 'https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=%s',
  56. // 添加媒体素材
  57. 'uploadMedia' => 'https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=%s',
  58. // 生成短连接
  59. 'shortUrl' => 'https://api.weixin.qq.com/cgi-bin/shorturl?access_token=%s',
  60. // 清除接口限制
  61. 'clearTokenQuota'=> 'https://api.weixin.qq.com/cgi-bin/clear_quota?access_token=%s',
  62. // 获取证书列表
  63. 'getCerts'=> 'https://api.mch.weixin.qq.com/v3/certificates',
  64. ];
  65. private static $jsApiUrl = [
  66. // jssdk 验证参数
  67. 'ticket' => 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=%s',
  68. // 永久TOKENresponseText
  69. 'token' => 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s',
  70. ];
  71. // 支付证书
  72. protected static $certPaths = [
  73. 'cert_path'=> WECHAT_PAY_CERT_PATH.'/apiclient_cert.pem',
  74. 'key_path'=> WECHAT_PAY_CERT_PATH.'/apiclient_key.pem',
  75. ];
  76. /**
  77. * 授权地址校验
  78. */
  79. public static function valid()
  80. {
  81. echo request()->get('echostr');
  82. exit;
  83. }
  84. /**
  85. * 微信注册初始化和授权登录
  86. */
  87. public static function auth()
  88. {
  89. $wxInfo = WechatService::getWechatInfo('', true);
  90. $openid = isset($wxInfo['openid'])? $wxInfo['openid'] : '';
  91. //$cacheKey = 'caches:weixin:init:'.get_client_ip().'_'.$openid;
  92. if (empty($wxInfo) || empty($openid)) {
  93. return false;
  94. }
  95. // 验证微信信息是否存在
  96. $wxData = [
  97. 'openid' => $openid,
  98. 'nickname' => isset($wxInfo['nickname']) ? $wxInfo['nickname'] : '',
  99. 'headimgurl' => isset($wxInfo['headimgurl']) ? $wxInfo['headimgurl'] : '',
  100. 'sex' => isset($wxInfo['sex']) ? $wxInfo['sex'] : 0,
  101. 'country' => isset($wxInfo['country']) ? $wxInfo['country'] : '',
  102. 'province' => isset($wxInfo['province']) ? $wxInfo['province'] : '',
  103. 'city' => isset($wxInfo['city']) ? $wxInfo['city'] : '',
  104. ];
  105. // 用户信息
  106. $inviteId = request()->get('sid');
  107. $inviteId = $inviteId? $inviteId : session('sid');
  108. $userData = [
  109. 'gender' => $wxData['sex'],
  110. 'openid' => $wxData['openid'],
  111. 'nickname' => $wxData['nickname'],
  112. 'login_time' => time(),
  113. 'login_ip' => get_client_ip(),
  114. 'avatar' => $wxData['headimgurl'],
  115. 'status'=> 1,
  116. ];
  117. // 微信用户不存在
  118. $userInfo = MemberModel::where(['openid' => $openid])
  119. ->where('status','>', 0)
  120. ->select(['id','openid','avatar'])
  121. ->first();
  122. $userInfo = $userInfo? $userInfo->toArray() : [];
  123. if (empty($userInfo)) {
  124. $userData['member_level'] = 1;
  125. $userData['password'] = get_password('123456');
  126. $userData['invite_id'] = intval($inviteId);
  127. $userData['code'] = makeUniqueCode($openid, 8);
  128. $userData['create_time'] = time();
  129. MemberModel::insertGetId($userData);
  130. } else{
  131. $updateData = [
  132. 'is_follow'=> isset($wxInfo['subscribe']) ? intval($wxInfo['subscribe']) : 0,
  133. 'login_time'=> time(),
  134. 'login_ip'=> get_client_ip(),
  135. 'update_time'=> time()
  136. ];
  137. MemberModel::where(['openid' => $openid])
  138. ->where('status','>=', 0)
  139. ->update($updateData);
  140. }
  141. // 记录OPENID
  142. $memberService = new MemberService();
  143. $field = ['m.id','m.openid','m.mobile','m.realname','m.nickname','m.avatar','m.login_time','m.status'];
  144. $userInfo = $memberService->getUserInfo(['m.openid'=> $openid], $field);
  145. return $userInfo;
  146. }
  147. /**
  148. * 跳转授权
  149. * @param string $url 回跳地址
  150. * @return mixed
  151. */
  152. public static function makeRedirectUrl($url)
  153. {
  154. $appid = WechatService::getConfigs('wx_appid');
  155. return sprintf(self::$apiUrl['auth'], $appid, urlencode($url), 'snsapi_userinfo');
  156. }
  157. /**
  158. * 获取配置
  159. * @param string $key 键名
  160. * @return array|mixed|string
  161. */
  162. public static function getConfigs($key = '')
  163. {
  164. $configService = new ConfigService();
  165. $defConfig = config('weixin.*');
  166. $notifyConfig = config('weixin.notify');
  167. $config = $configService->getConfigByGroup(7);
  168. $config = $config ? $config : $defConfig;
  169. $config['notify'] = $notifyConfig? $notifyConfig : [];
  170. if ($key && $key != 'notify') {
  171. return isset($config[$key]['value']) ? $config[$key]['value'] : '';
  172. } else if ($key == 'notify'){
  173. return isset($config['notify'])? $config['notify'] : [];
  174. } else {
  175. return $config ? $config : [];
  176. }
  177. }
  178. /**
  179. * 获取ACCESS_TOKEN
  180. * @return bool|string
  181. */
  182. public static function getTempAccessToken($key = '', $refresh = false)
  183. {
  184. $code= request()->get('code','');
  185. $appid = WechatService::getConfigs('wx_appid');
  186. $appsecret = WechatService::getConfigs('wx_appsecret');
  187. $cacheKey = 'caches:tokens:access_temp:' . $code;
  188. $tokenData = RedisService::get($cacheKey);
  189. if (empty($tokenData) || $refresh) {
  190. $url = sprintf(self::$apiUrl['tempToken'], $code, $appid, $appsecret);
  191. $tokenData = httpRequest($url);
  192. RedisService::set("caches:tokens:result:temp_{$code}", $tokenData, 3600);
  193. $code = isset($tokenData['errcode']) ? $tokenData['errcode'] : '';
  194. if ($code || empty($tokenData)) {
  195. return $tokenData;
  196. }
  197. $token = isset($tokenData['access_token']) ? $tokenData['access_token'] : '';
  198. $openid = isset($tokenData['openid']) ? $tokenData['openid'] : '';
  199. $tokenData = [
  200. 'token' => $token,
  201. 'openid' => $openid,
  202. 'data' => $tokenData,
  203. 'date' => date('Y-m-d H:i:s'),
  204. 'expire' => time() + 7000,
  205. ];
  206. RedisService::set($cacheKey, $tokenData, 7200);
  207. }
  208. $expire = isset($tokenData['expire']) ? intval($tokenData['expire']) : 0;
  209. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  210. if (($expire && $expire < time()) || empty($token)) {
  211. $tokenData = WechatService::getTempAccessToken($key, true);
  212. }
  213. if ($key) {
  214. $tokenData = isset($tokenData[$key]) ? trim($tokenData[$key]) : '';
  215. }
  216. return $tokenData;
  217. }
  218. /**
  219. * 获取ACCESS_TOKEN
  220. * @return bool|string
  221. */
  222. public static function getAccessToken($key = '', $refresh = false)
  223. {
  224. $code= request()->get('code','');
  225. $code = $code? $code : session('code');
  226. session('code', $code);
  227. $appid = WechatService::getConfigs('wx_appid');
  228. $appsecret = WechatService::getConfigs('wx_appsecret');
  229. $cacheKey = 'caches:tokens:access_' . $appid . '_' . $appsecret;
  230. $tokenData = RedisService::get($cacheKey);
  231. if (empty($tokenData) || $refresh) {
  232. $url = sprintf(self::$apiUrl['accessToken'], $appid, $appsecret);
  233. $tokenData = httpRequest($url);
  234. RedisService::set("caches:tokens:result:{$code}", $tokenData, 3600);
  235. $code = isset($tokenData['errcode']) ? $tokenData['errcode'] : '';
  236. if ($code || empty($tokenData)) {
  237. return $tokenData;
  238. }
  239. $token = isset($tokenData['access_token']) ? $tokenData['access_token'] : '';
  240. $openid = isset($tokenData['openid']) ? $tokenData['openid'] : '';
  241. $tokenData = [
  242. 'token' => $token,
  243. 'openid' => $openid,
  244. 'data' => $tokenData,
  245. 'date' => date('Y-m-d H:i:s'),
  246. 'expire' => time() + 7000,
  247. ];
  248. RedisService::set($cacheKey, $tokenData, 7200);
  249. }
  250. $expire = isset($tokenData['expire']) ? intval($tokenData['expire']) : 0;
  251. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  252. if (($expire && $expire < time()) || empty($token)) {
  253. $tokenData = WechatService::getAccessToken($key, true);
  254. }
  255. if ($key) {
  256. $tokenData = isset($tokenData[$key]) ? trim($tokenData[$key]) : '';
  257. }
  258. return $tokenData;
  259. }
  260. /**
  261. * 获取微信UserInfo用户信息
  262. * @param string $openid 获取的用户OPENID,默认当前用户
  263. * @return mixed
  264. */
  265. public static function getWechatInfo($curOpenid = '', $saveData = false)
  266. {
  267. $code= request()->get('code','');
  268. $tokenData = WechatService::getTempAccessToken();
  269. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  270. $openid = isset($tokenData['openid']) ? trim($tokenData['openid']) : '';
  271. if (empty($token)) {
  272. return false;
  273. }
  274. $openid = $curOpenid ? $curOpenid : $openid;
  275. $url = sprintf(self::$apiUrl['wxInfo'], $token, $openid);
  276. RedisService::set("caches:userInfo:request_{$openid}",['token'=> $tokenData,'url'=> $url], 600);
  277. $result = httpRequest($url);
  278. $errcode = isset($result['errcode']) ? $result['errcode'] : '';
  279. RedisService::set('caches:weixin:userInfo:result_'.$openid, $result, 600);
  280. if (empty($result) || $errcode) {
  281. RedisService::keyDel('caches:tokens:access_temp:' . $code);
  282. return false;
  283. }
  284. if ($saveData) {
  285. $wxData = [
  286. 'openid' => $openid,
  287. 'nickname' => isset($result['nickname']) ? $result['nickname'] : '',
  288. 'headimgurl' => isset($result['headimgurl']) ? $result['headimgurl'] : '',
  289. 'sex' => isset($result['sex']) ? $result['sex'] : 0,
  290. 'country' => isset($result['country']) ? $result['country'] : '',
  291. 'province' => isset($result['province']) ? $result['province'] : '',
  292. 'city' => isset($result['city']) ? $result['city'] : '',
  293. ];
  294. if (empty(FansModel::where(['openid' => $openid])->value('id'))) {
  295. FansModel::insertGetId($wxData);
  296. } else {
  297. FansModel::where(['openid' => $openid])->update($wxData);
  298. }
  299. }
  300. return $result;
  301. }
  302. /**
  303. * 获取JSSDK ticket参数
  304. * @author wesmiler
  305. */
  306. private static function getTicket($refresh = false, $refreshToken = false)
  307. {
  308. $appid = WechatService::getConfigs('wx_appid');
  309. $appsecret = WechatService::getConfigs('wx_appsecret');
  310. $cacheKey = 'caches:tokens:jsapiTicket:' . $appid . '_' . $appsecret;
  311. $ticketData = RedisService::get($cacheKey);
  312. $ticket = isset($ticketData['ticket']) ? $ticketData['ticket'] : '';
  313. if (empty($ticket) || $refresh) {
  314. $tokenData = WechatService::getAccessToken('', $refreshToken);
  315. $code = isset($tokenData['errcode']) ? $tokenData['errcode'] : '';
  316. if ($code) {
  317. return $tokenData;
  318. }
  319. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  320. $url = sprintf(self::$jsApiUrl['ticket'], $token);
  321. $result = httpRequest($url);
  322. if (empty($result)) {
  323. RedisService::set('caches:tokens:jsapiTicket_error', $result, 3600);
  324. return false;
  325. }
  326. $ticket = isset($result['ticket']) ? $result['ticket'] : '';
  327. $ticketData = [
  328. 'ticket' => $ticket,
  329. 'expire' => time() + 6000,
  330. ];
  331. RedisService::set($cacheKey, $ticketData, 7200);
  332. }
  333. $expire = isset($ticketData['expire']) ? intval($ticketData['expire']) : 0;
  334. if (empty($expire) || $expire < time()) {
  335. $ticket = WechatService::getTicket(true);
  336. }
  337. if (empty($ticket)) {
  338. $ticket = WechatService::getTicket(true, true);
  339. }
  340. return $ticket;
  341. }
  342. /**
  343. * 获取JSSDK签名参数
  344. * @param string $url 请求地址
  345. * @return array
  346. */
  347. public static function getJssdkParams($url = '')
  348. {
  349. // token请求次数超出警告范围
  350. $countKey = "caches:tokens:count";
  351. $requestCount = RedisService::get($countKey);
  352. if($requestCount>=5000){
  353. return ['error'=> 'token请求失败次数已超出警告值5000'];
  354. }
  355. $countKey = "caches:tokens:ticketCount:".get_client_ip();
  356. $requestCount = RedisService::get($countKey);
  357. if($requestCount>=100){
  358. return ['error'=> '分享参数请求次数过多请稍后重试'];
  359. }
  360. $result = WechatService::getTicket();
  361. $url = $url ? $url : Input::url();
  362. $code = isset($result['errcode']) ? $result['errcode'] : '';
  363. if ($code) {
  364. return $result;
  365. }
  366. $params = [
  367. 'jsapi_ticket' => $result,
  368. 'noncestr' => uniqid('J'),
  369. 'timestamp' => time(),
  370. 'url' => $url,
  371. ];
  372. RedisService::set($countKey, $requestCount+1, 30);
  373. $signature = WechatService::getJssdkSign($params);
  374. return [
  375. 'appId' => WechatService::getConfigs('wx_appid'),
  376. 'timestamp' => $params['timestamp'],
  377. 'nonceStr' => $params['noncestr'],
  378. 'signature' => $signature,
  379. 'url' => $url,
  380. ];
  381. }
  382. /**
  383. * 获取JSSDK 签名
  384. * @param $params 签名参数
  385. * @return string
  386. */
  387. private static function getJssdkSign($params)
  388. {
  389. $str = [];
  390. ksort($params);
  391. foreach ($params as $k => $val) {
  392. $str[] = $k . '=' . $val;
  393. }
  394. $str = implode('&', $str);
  395. return sha1($str);
  396. }
  397. /**
  398. * jsapi统一下单V3
  399. * @param $order 订单参数
  400. * @author wesmiler
  401. * @return array
  402. */
  403. public static function jsapiUnifiedorder($order, $scene = 'jsapiPay')
  404. {
  405. $appId = WechatService::getConfigs('wx_appid');
  406. $spAppId = WechatService::getConfigs('wx_sp_appid');
  407. $spMchId = WechatService::getConfigs('wx_sp_mchid');
  408. $mchId = WechatService::getConfigs('wx_mch_id');
  409. $serial = WechatService::getConfigs('wx_mch_cert_no');
  410. $notifyUrls = WechatService::getConfigs('notify');
  411. var_dump($notifyUrls);
  412. $notifyUrl = isset($notifyUrls[$scene]) ? url()->formatRoot('http://').$notifyUrls[$scene] : url()->formatRoot('http://').'/api/notify/pay/index';
  413. $openid = isset($order['openid']) ? trim($order['openid']) : '';
  414. $orderNo = isset($order['orderNo']) ? trim($order['orderNo']) : '';
  415. $totalFee = isset($order['amount']) ? moneyFormat($order['amount']) : 0.00;
  416. // 测试支付金额
  417. $payDebug = config('weixin.payDebug');
  418. if ($payDebug) {
  419. $totalFee = 0.01;
  420. }
  421. if (empty($openid) || empty($orderNo) || empty($totalFee)) {
  422. return ['code' => 'error', 'message' => '参数错误'];
  423. }
  424. $unified = array(
  425. 'sp_appid' => $spAppId,
  426. 'sub_appid' => $appId,
  427. 'attach' => 'pay', //商家数据包,原样返回,如果填写中文,请注意转换为utf-8
  428. 'description' => isset($order['body']) ? trim($order['body']) : '订单支付',
  429. 'sp_mchid' => $spMchId,
  430. 'sub_mchid' => $mchId,
  431. 'notify_url' => $notifyUrl,
  432. 'payer'=> [
  433. 'sub_openid' => $openid, //子商户此参数必传
  434. ],
  435. 'out_trade_no' => $orderNo,
  436. 'amount' => [
  437. 'total'=> intval($totalFee * 100),
  438. 'currency'=> 'CNY'
  439. ], //单位 转为分
  440. 'scene_info' => [
  441. 'payer_client_ip'=> get_client_ip()
  442. ],
  443. );
  444. $body = json_encode($unified);
  445. $url = !empty(self::$apiUrl['unifiedorderV3']) ? trim(self::$apiUrl['unifiedorderV3']) : 'https://api.mch.weixin.qq.com/v3/pay/partner/transactions/jsapi';
  446. $token = WechatService::getSignToken(['url'=> $url, 'method'=> 'POST','mchid'=> $spMchId, 'body'=> $body]);
  447. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedSign', ['data'=>$unified,'token'=> $token], 600);
  448. $headers = ["Authorization: {$token}","Content-Type: application/json","Accept: application/json","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"];
  449. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedRequest', ['data'=> $unified,'headers'=> $headers], 600);
  450. $response = WechatService::curlPost($url, $body,[],[],$headers);
  451. $response = $response? json_decode($response, true) : $response;
  452. //禁止引用外部xml实体
  453. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedResult', ['data'=> $response], 600);
  454. $prepayId = isset($response['prepay_id'])? $response['prepay_id'] : '';
  455. $code = isset($response['code'])? $response['code'] : '';
  456. if (!$prepayId) {
  457. $message = isset($response['message'])? $response['message'] : 'prepayid get fail';
  458. return ['code' => 'error', 'message' => $message,'result'=> $response];
  459. }
  460. // 返回支付参数
  461. return WechatService::getJsapiPareamsV3($response, $unified);
  462. }
  463. /**
  464. * jsapi统一下单V2
  465. * @param $order 订单参数
  466. * @author wesmiler
  467. * @return array
  468. */
  469. public static function jsapiUnifiedorderV2($order, $scene = 'jsapiPay')
  470. {
  471. $appId = WechatService::getConfigs('wx_appid');
  472. $mchId = WechatService::getConfigs('wx_mch_id');
  473. $notifyUrls = WechatService::getConfigs('notify');
  474. $notifyUrl = isset($notifyUrls[$scene]) ? url()->formatRoot('http://').$notifyUrls[$scene] : url()->formatRoot('http://').'/api/notify/pay/index';
  475. $openid = isset($order['openid']) ? trim($order['openid']) : '';
  476. $orderNo = isset($order['orderNo']) ? trim($order['orderNo']) : '';
  477. $totalFee = isset($order['amount']) ? moneyFormat($order['amount']) : 0.00;
  478. // 测试支付金额
  479. $payDebug = config('weixin.payDebug');
  480. if ($payDebug) {
  481. $totalFee = 0.01;
  482. }
  483. if (empty($openid) || empty($orderNo) || empty($totalFee)) {
  484. return ['code' => 'error', 'message' => '参数错误'];
  485. }
  486. $unified = array(
  487. 'appid' => $appId,
  488. 'attach' => 'pay', //商家数据包,原样返回,如果填写中文,请注意转换为utf-8
  489. 'body' => isset($order['body']) ? trim($order['body']) : '订单支付',
  490. 'mch_id' => $mchId,
  491. 'nonce_str' => WechatService::createNonceStr(),
  492. 'notify_url' => $notifyUrl,
  493. 'openid'=> $openid,
  494. 'out_trade_no' => $orderNo,
  495. 'spbill_create_ip' => get_client_ip(),
  496. 'total_fee' => intval($totalFee * 100), //单位 转为分
  497. 'trade_type' => 'JSAPI',
  498. );
  499. RedisService::set('caches:orders:'.$scene.':'.$openid.':unified', $unified, 600);
  500. $unified['sign'] = WechatService::getPaySign($unified);
  501. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedSign', $unified, 600);
  502. $url = !empty(self::$apiUrl['unifiedorder']) ? trim(self::$apiUrl['unifiedorder']) : 'https://api.mch.weixin.qq.com/pay/unifiedorder';
  503. $data = WechatService::arrayToXml($unified);
  504. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedXml', ['data'=> $unified,'result'=> $data], 600);
  505. $responseXml = WechatService::curlPost($url, $data);
  506. //禁止引用外部xml实体
  507. libxml_disable_entity_loader(true);
  508. $unifiedOrder = simplexml_load_string($responseXml, 'SimpleXMLElement', LIBXML_NOCDATA);
  509. $unifiedOrder = (array)$unifiedOrder;
  510. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedResult', ['data'=> $unifiedOrder,'result'=> $data], 600);
  511. if ($unifiedOrder === false) {
  512. return ['code' => 'exception', 'message' => 'parase xml error'];
  513. }
  514. if (isset($unifiedOrder['return_code']) && $unifiedOrder['return_code'] != 'SUCCESS') {
  515. return ['code' => 'error', 'message' => $unifiedOrder['return_msg']];
  516. }
  517. if (isset($unifiedOrder['result_code']) && $unifiedOrder['result_code'] != 'SUCCESS') {
  518. return ['code' => 'error', 'message' => $unifiedOrder['err_code']];
  519. }
  520. // 返回支付参数
  521. return WechatService::getJsapiPareams($unifiedOrder, $unified);
  522. }
  523. /**
  524. * 提现打款
  525. * @param $order
  526. * @param string $scene
  527. * @return array|string[]
  528. */
  529. public static function transferOrder($order, $scene='withdraw'){
  530. $appId = WechatService::getConfigs('wx_appid');
  531. $mchId = WechatService::getConfigs('wx_mch_id');
  532. $openid = isset($order['openid']) ? trim($order['openid']) : '';
  533. $orderNo = isset($order['orderNo']) ? trim($order['orderNo']) : '';
  534. $totalFee = isset($order['amount']) ? moneyFormat($order['amount']) : 0.00;
  535. // 测试支付金额
  536. $payDebug = config('weixin.payDebug');
  537. if ($payDebug) {
  538. $totalFee = 0.3;
  539. }
  540. if (empty($openid) || empty($orderNo) || empty($totalFee)) {
  541. return ['code' => 'error', 'message' => '参数错误'];
  542. }
  543. $unified = array(
  544. 'mch_appid' => $appId,
  545. 'mchid' => trim($mchId),
  546. 'device_info' => uniqid(),
  547. 'nonce_str' => WechatService::createNonceStr(),
  548. 'partner_trade_no' => $orderNo,
  549. 'openid' => $openid,
  550. 'check_name' => isset($order['check_name']) && $order['check_name']? trim($order['check_name']) : 'NO_CHECK', // 是否校验真实姓名
  551. 'amount' => intval($totalFee * 100), //单位 转为分
  552. 'desc' => isset($order['body']) ? trim($order['body']) : '余额提现',
  553. 'spbill_create_ip' => get_client_ip(),
  554. );
  555. // 是否校验真实姓名
  556. if($unified['check_name'] == 'FORCE_CHECK'){
  557. $unified['re_user_name'] = isset($order['real_name']) ? trim($order['real_name']) : '';
  558. }
  559. RedisService::set('caches:orders:'.$scene.':'.$openid.':unified', $unified, 600);
  560. $unified['sign'] = WechatService::getPaySign($unified);
  561. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedSign', $unified, 600);
  562. $url = !empty(self::$apiUrl['transfers']) ? trim(self::$apiUrl['transfers']) : 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
  563. $data = WechatService::arrayToXml($unified);
  564. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedXml', ['data'=> $unified,'result'=> $data], 600);
  565. $responseXml = WechatService::curlPost($url, $data, [], self::$certPaths);
  566. //禁止引用外部xml实体
  567. libxml_disable_entity_loader(true);
  568. $result = simplexml_load_string($responseXml, 'SimpleXMLElement', LIBXML_NOCDATA);
  569. $result = (array)$result;
  570. RedisService::set('caches:orders:'.$scene.':'.$openid.':unifiedResult', ['data'=> $result,'result'=> $data], 600);
  571. if ($result === false) {
  572. return ['code' => 'exception', 'message' => 'parase xml error'];
  573. }
  574. if (isset($result['return_code']) && $result['return_code'] != 'SUCCESS') {
  575. return ['code' => 'error', 'message' => WechatService::getError($result['return_msg']),'type'=>'return_code'];
  576. }
  577. if (isset($result['result_code']) && $result['result_code'] != 'SUCCESS') {
  578. return ['code' => 'error', 'message' => $result['err_code_des'],'error_code'=> $result['err_code'],'type'=>'result_code'];
  579. }
  580. return $result;
  581. }
  582. /**
  583. * 查询企业付款订单
  584. * @param $trane_order_no 订单号
  585. * @return string[]
  586. */
  587. public static function queryTransferOrder($trane_order_no){
  588. $appId = WechatService::getConfigs('wx_appid');
  589. $mchId = WechatService::getConfigs('wx_mch_id');
  590. if (empty($trane_order_no)) {
  591. return ['code' => 'error', 'message' => '参数错误'];
  592. }
  593. $unified = array(
  594. 'wx_appid' => $appId,
  595. 'wx_mch_id' => trim($mchId),
  596. 'nonce_str' => WechatService::createNonceStr(),
  597. 'partner_trade_no' => $trane_order_no,
  598. );
  599. RedisService::set('orders:transfer:'.$trane_order_no.':unified', $unified, 600);
  600. $unified['sign'] = WechatService::getPaySign($unified);
  601. RedisService::set('orders:transfer:'.$trane_order_no.':unifiedSign', $unified, 600);
  602. $url = !empty(self::$apiUrl['queryTransfer']) ? trim(self::$apiUrl['queryTransfer']) : 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo';
  603. $data = WechatService::arrayToXml($unified);
  604. RedisService::set('orders:transfer:'.$trane_order_no.':unifiedXml', ['data'=> $unified,'result'=> $data], 600);
  605. $responseXml = WechatService::curlPost($url, $data, [], self::$certPaths);
  606. //禁止引用外部xml实体
  607. libxml_disable_entity_loader(true);
  608. $result = simplexml_load_string($responseXml, 'SimpleXMLElement', LIBXML_NOCDATA);
  609. $result = (array)$result;
  610. RedisService::set('orders:transfer:'.$trane_order_no.':unifiedResult', ['data'=> $result,'result'=> $data], 600);
  611. if ($result === false) {
  612. return ['code' => 'exception', 'message' => 'parase xml error'];
  613. }
  614. if (isset($result['return_code']) && $result['return_code'] != 'SUCCESS') {
  615. return ['code' => 'error', 'message' => WechatService::getError($result['return_msg']),'type'=>'return_code'];
  616. }
  617. if (isset($result['result_code']) && $result['result_code'] != 'SUCCESS') {
  618. return ['code' => 'error', 'message' => $result['err_code_des'],'error_code'=> $result['err_code'],'type'=>'result_code'];
  619. }
  620. return $result;
  621. }
  622. /**
  623. * 获取JSAPI支付签名参数
  624. * @param $unifiedOrder 统一下单结果
  625. * @param array $unified 提交统一下单参数
  626. * @return array
  627. */
  628. public static function getJsapiPareams($unifiedOrder, $unified = [])
  629. {
  630. $time = time();
  631. $prepayId = isset($unifiedOrder['prepay_id']) ? $unifiedOrder['prepay_id'] : '';
  632. $params = array(
  633. "appId" => WechatService::getConfigs('wx_appid'),
  634. "timeStamp" => "$time", //这里是字符串的时间戳,不是int,所以需加引号
  635. "nonceStr" => isset($unified['nonce_str']) ? trim($unified['nonce_str']) : WechatService::createNonceStr(),
  636. "package" => "prepay_id=" . $prepayId,
  637. "signType" => 'MD5',
  638. );
  639. // 重签名
  640. $params['paySign'] = WechatService::getPaySign($params);
  641. $params['prepay_id'] = $prepayId;
  642. return $params;
  643. }
  644. /**
  645. * 获取JSAPI支付签名参数
  646. * @param $unifiedOrder 统一下单结果
  647. * @param array $unified 提交统一下单参数
  648. * @return array
  649. */
  650. public static function getJsapiPareamsV3($unifiedOrder, $unified = [])
  651. {
  652. $time = time();
  653. $prepayId = isset($unifiedOrder['prepay_id']) ? $unifiedOrder['prepay_id'] : '';
  654. $params = array(
  655. "appId" => WechatService::getConfigs('wx_appid'),
  656. "timeStamp" => "$time", //这里是字符串的时间戳,不是int,所以需加引号
  657. "nonceStr" => isset($unified['nonce_str']) ? trim($unified['nonce_str']) : WechatService::createNonceStr(32),
  658. "package" => "prepay_id=" . $prepayId,
  659. );
  660. // 重签名
  661. $params['paySign'] = WechatService::getPaySignV3($params);
  662. $params['signType'] = 'RSA';
  663. $params['prepay_id'] = $prepayId;
  664. return $params;
  665. }
  666. /**
  667. * 查询订单
  668. * @param $outTradeNo 单号
  669. * @return bool|\SimpleXMLElement
  670. */
  671. public static function queryOrder($outTradeNo)
  672. {
  673. $params['wx_appid'] = WechatService::getConfigs('wx_appid');
  674. $params['wx_mch_id'] = WechatService::getConfigs('wx_mch_id');
  675. $params['nonce_str'] = WechatService::createNonceStr();
  676. $params['out_trade_no'] = $outTradeNo;
  677. //获取签名数据
  678. $params['sign'] = WechatService::getPaySign($params);
  679. $responseXml = WechatService::curlPost(self::$apiUrl['queryOrder'], WechatService::arrayToXml($params));
  680. $result = WechatService::xmlToArray($responseXml);
  681. $returnCode = isset($result['return_code']) ? $result['return_code'] : '';
  682. $tradState = isset($result['trade_state']) ? $result['trade_state'] : '';
  683. $resultCode = isset($result['result_code']) ? $result['result_code'] : '';
  684. if ($resultCode && $returnCode && $tradState) {
  685. return $result;
  686. } else {
  687. return false;
  688. }
  689. }
  690. /**
  691. * XML转数组
  692. * @param $xml
  693. * @return bool|\SimpleXMLElement
  694. */
  695. private static function xmlToArray($xml)
  696. {
  697. if (empty($xml)) return false;
  698. libxml_disable_entity_loader(true);
  699. return simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
  700. }
  701. /**
  702. * 生成随机字符串
  703. * @param int $length 长度
  704. * @return string
  705. */
  706. public static function createNonceStr($length = 16)
  707. {
  708. $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  709. $str = '';
  710. for ($i = 0; $i < $length; $i++) {
  711. $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
  712. }
  713. return $str;
  714. }
  715. /**
  716. * @param $params
  717. * @param $key
  718. * @return string
  719. */
  720. public static function getPaySign($params, $key = '')
  721. {
  722. ksort($params, SORT_STRING);
  723. $key = $key ? $key : WechatService::getConfigs('wx_pay_key');
  724. $unSignParaString = WechatService::formatParams($params, false);
  725. //echo $unSignParaString.'++'.$key;
  726. $signStr = strtoupper(md5($unSignParaString . "&key=" . $key));
  727. return $signStr;
  728. }
  729. /**
  730. * 获取V3签名参数
  731. * @param $params
  732. * @param $key
  733. * @return string
  734. */
  735. public static function getPaySignV3($params, $key = '')
  736. {
  737. $appId = isset($params['appId'])? $params['appId'] : '';
  738. $timeStamp = isset($params['timeStamp'])? $params['timeStamp'] : '';
  739. $nonceStr = isset($params['nonceStr'])? $params['nonceStr'] : '';
  740. unset($params['signType']);
  741. $signStr = implode("\n", array_values($params))."\n";
  742. //echo $signStr;
  743. $mch_private_key = openssl_get_privatekey(file_get_contents(self::$certPaths['key_path']));
  744. openssl_sign($signStr, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
  745. $sign = base64_encode($raw_sign);
  746. return $sign;
  747. }
  748. /**
  749. * 回调数据解密
  750. * @param $notifyData 回调数据对象
  751. * @return false|string
  752. * @throws \SodiumException
  753. */
  754. public static function decryptNotifyData($notifyData){
  755. $md5Key = WechatService::getConfigs('wx_pay_key');
  756. $ciphertext = isset($notifyData['ciphertext'])? $notifyData['ciphertext'] : '';
  757. $associatedData = isset($notifyData['associated_data'])? $notifyData['associated_data'] : '';
  758. $nonce = isset($notifyData['nonce'])? $notifyData['nonce'] : '';
  759. $ciphertext = base64_decode($ciphertext);
  760. return sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonce, $md5Key);
  761. }
  762. /**
  763. * 验证JSAPI回调V3
  764. * @param $notifyData
  765. * @return array|bool
  766. */
  767. public static function checkJsapiNotifyV3($signStr, $sign, $notifyData=[])
  768. {
  769. $tradeState = isset($notifyData['trade_state'])? $notifyData['trade_state'] : false;
  770. if($notifyData && $tradeState != 'SUCCESS'){
  771. return false;
  772. }
  773. $transaction_id = isset($notifyData['transaction_id'])? $notifyData['transaction_id'] : '';
  774. if($notifyData && empty($transaction_id)){
  775. return false;
  776. }
  777. // 证书序列号
  778. $serial = WechatService::getConfigs('wx_mch_cert_no');
  779. $checkSerial = isset($notifyData['serial'])? $notifyData['serial'] : '';
  780. if($serial != $checkSerial){
  781. //return false;
  782. }
  783. return true;
  784. $sign = stripslashes($sign);
  785. $signature = base64_decode($sign);
  786. $publicKey = openssl_pkey_get_public(file_get_contents(self::$certPaths['cert_path']));
  787. return openssl_verify($signStr, $signature, $publicKey, 'sha256WithRSAEncryption');
  788. }
  789. /**
  790. * 验证JSAPI回调
  791. * @param $notifyData
  792. * @return array|bool
  793. */
  794. public static function checkJsapiNotify($notifyData)
  795. {
  796. if (empty($notifyData)) {
  797. return ['code' => 'error', 'message' => 'parse xml error'];
  798. }
  799. $returnCode = isset($notifyData['return_code']) ? trim($notifyData['return_code']) : '';
  800. $resultCode = isset($notifyData['result_code']) ? trim($notifyData['result_code']) : '';
  801. $nofitySign = isset($notifyData['sign']) ? trim($notifyData['sign']) : '';
  802. if ($returnCode != 'SUCCESS') {
  803. $error = isset($notifyData['return_msg']) ? $notifyData['return_msg'] : '';
  804. return ['code' => 'error', 'message' => $error];
  805. }
  806. if ($resultCode != 'SUCCESS') {
  807. $error = isset($notifyData['err_code']) ? $notifyData['err_code'] : '';
  808. return ['code' => 'error', 'message' => $error];
  809. }
  810. // 验证签名
  811. unset($notifyData['sign']);
  812. $sign = WechatService::getPaySign($notifyData);
  813. if ($nofitySign == $sign) {
  814. echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
  815. return true;
  816. }
  817. return false;
  818. }
  819. /**
  820. * 请求接口
  821. * @param string $url 地址
  822. * @param string $postData xml参数
  823. * @param array $options
  824. * @return mixed
  825. */
  826. public static function curlPost($url = '', $postData = '', $options = array(), $cert=[], $headers=[])
  827. {
  828. if (is_array($postData)) {
  829. $postData = http_build_query($postData);
  830. }
  831. $ch = curl_init();
  832. curl_setopt($ch, CURLOPT_URL, $url);
  833. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  834. curl_setopt($ch, CURLOPT_POST, 1);
  835. curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
  836. curl_setopt($ch, CURLOPT_TIMEOUT, 30); //设置cURL允许执行的最长秒数
  837. if (!empty($options)) {
  838. curl_setopt_array($ch, $options);
  839. }
  840. if($headers){
  841. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  842. }
  843. if($cert){
  844. curl_setopt($ch,CURLOPT_HEADER,FALSE);
  845. curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
  846. if(isset($cert['cert_path']) && $cert['cert_path']){
  847. curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
  848. curl_setopt($ch,CURLOPT_SSLCERT, $cert['cert_path']);
  849. }
  850. if(isset($cert['key_path']) && $cert['key_path']) {
  851. curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
  852. curl_setopt($ch, CURLOPT_SSLKEY, $cert['key_path']);
  853. }
  854. }
  855. //https请求 不验证证书和host
  856. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  857. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  858. $data = curl_exec($ch);
  859. curl_close($ch);
  860. return $data;
  861. }
  862. /**
  863. * 数组转XML
  864. * @param $arr 数组数据
  865. * @return string
  866. */
  867. public static function arrayToXml($arr)
  868. {
  869. $xml = "<xml>";
  870. foreach ($arr as $key => $val) {
  871. if ($key == 'detail') {
  872. $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
  873. } else {
  874. $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
  875. }
  876. }
  877. $xml .= "</xml>";
  878. return $xml;
  879. }
  880. /**
  881. * 签名参数格式化
  882. * @param $paraMap 参数
  883. * @param bool $urlEncode 是否编码
  884. * @return bool|string
  885. */
  886. protected static function formatParams($paraMap, $urlEncode = false)
  887. {
  888. $buff = "";
  889. ksort($paraMap);
  890. foreach ($paraMap as $k => $v) {
  891. if (null != $v && "null" != $v) {
  892. if ($urlEncode) {
  893. $v = urlencode($v);
  894. }
  895. $buff .= $k . "=" . $v . "&";
  896. }
  897. }
  898. $reqPar = '';
  899. if (strlen($buff) > 0) {
  900. $reqPar = substr($buff, 0, strlen($buff) - 1);
  901. }
  902. return $reqPar;
  903. }
  904. /**
  905. * 生成微信二维码
  906. * @param int $sourceId 来源ID
  907. * @param $sceneStr 场景参数字符串或ID
  908. * @param $scene 场景标识:qrcode-用户二维码
  909. * @param string $qrType 二维码生成类型:QR_SCENE, QR_STR_SCENE, QR_LIMIT_SCENE, QR_LIMIT_STR_SCENE
  910. * @param int $expire 有效期,配合场景类型使用,临时二维码最长30天有效期,0-永久
  911. * @return array|bool
  912. * @throws \think\Exception
  913. * @throws \think\db\exception\DataNotFoundException
  914. * @throws \think\db\exception\ModelNotFoundException
  915. * @throws \think\exception\DbException
  916. * @throws \think\exception\PDOException
  917. */
  918. public static function makeQrcode($sourceId = 0, $sceneStr = '', $scene = 'qrcode', $qrType = 'QR_STR_SCENE', $expire = -1)
  919. {
  920. $expire = $expire>=0 ? $expire : 24 * 3600 * 20;
  921. $tokenData = WechatService::getAccessToken('');
  922. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  923. $qrData = db('qrcode')
  924. ->where(['source_id' => $sourceId, 'scene' => $scene, 'status' => 1])
  925. ->field('id,source_id,ticket,url,expire_at')
  926. ->find();
  927. $qrcodeId = isset($qrData['id']) ? $qrData['id'] : 0;
  928. $qrcodeExpire = isset($qrData['expire_at']) ? strtotime($qrData['expire_at']) : 0;
  929. if (($expire==0 && $qrcodeId) || $qrcodeExpire > time() && $qrcodeId) {
  930. $ticket = isset($qrData['ticket']) ? $qrData['ticket'] : '';
  931. $qrcode = sprintf(self::$apiUrl['getQrcodeByTicket'], $ticket);
  932. $qrData['qrcode'] = WechatService::loadImage($qrcode, $scene);
  933. if($qrData['qrcode']){
  934. return $qrData;
  935. }
  936. }
  937. if (empty($token)) {
  938. $tokenData = WechatService::getAccessToken('', true);
  939. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  940. if (empty($token)) {
  941. return 1010;
  942. }
  943. }
  944. if (!in_array($qrType, ['QR_SCENE', 'QR_STR_SCENE', 'QR_LIMIT_SCENE', 'QR_LIMIT_STR_SCENE'])) {
  945. return 2111;
  946. }
  947. $data = [
  948. 'expire_seconds' => $expire,
  949. 'action_name' => $qrType,
  950. ];
  951. if (in_array($qrType, ['QR_SCENE', 'QR_LIMIT_SCENE'])) {
  952. $data['action_info'] = ['scene' => ['scene_id' => $sceneStr]];
  953. } else {
  954. $data['action_info'] = ['scene' => ['scene_str' => $scene . '_' . $sceneStr]];
  955. }
  956. $url = sprintf(self::$apiUrl['makeQrcode'], $token);
  957. $result = httpRequest($url, json_encode($data, 256));
  958. $ticket = isset($result['ticket']) ? $result['ticket'] : '';
  959. RedisService::set('qrcodes:result', ['url'=> $url, 'params'=> $data, 'result'=> $result], 600);
  960. if ($result && $ticket) {
  961. $qrData = [
  962. 'source_id' => $sourceId,
  963. 'scene' => $scene,
  964. 'scene_str' => $sceneStr,
  965. 'ticket' => $ticket,
  966. 'expire_at' => $expire>0? date('Y-m-d H:i:s', time() + $expire) : null,
  967. 'url' => isset($result['url']) ? $result['url'] : '',
  968. ];
  969. if ($qrcodeId) {
  970. $qrData['updated_at'] = date('Y-m-d H:i:s');
  971. //$qrcodeId = db('qrcode')->where(['id' => $qrcodeId])->update($qrData);
  972. } else {
  973. $qrData['created_at'] = date('Y-m-d H:i:s');
  974. //$qrcodeId = db('qrcode')->insertGetId($qrData);
  975. $qrData['id'] = $qrcodeId;
  976. }
  977. $qrcode = sprintf(self::$apiUrl['getQrcodeByTicket'], $ticket);
  978. $qrData['qrcode'] = WechatService::loadImage($qrcode, $scene, true);
  979. }
  980. return $qrcodeId > 0 ? $qrData : 1009;
  981. }
  982. /**
  983. * 下载图片
  984. * @param $file 远程文件
  985. * @param string $type 类型
  986. * @return bool|string
  987. */
  988. public static function loadImage($file, $type='qrcode', $refresh=false){
  989. if(empty($file)){
  990. return false;
  991. }
  992. $key = "caches:members:{$type}:".md5($file);
  993. $qrcode = RedisService::get($key);
  994. if(empty($qrcode) || $refresh){
  995. $qrcodeContent = file_get_contents($file);
  996. if($qrcodeContent){
  997. if(!is_dir("upload/{$type}/weixin/")){
  998. mkdir("upload/{$type}/weixin/", 0755, true);
  999. }
  1000. $qrcode = "{$type}/weixin/QR_".md5($file).'.jpg';
  1001. file_put_contents("upload/".$qrcode, $qrcodeContent);
  1002. RedisService::set($key, $qrcode, 7 * 24 * 3600);
  1003. }
  1004. }
  1005. if(!file_exists('./upload/'.$qrcode)){
  1006. return false;
  1007. }
  1008. return get_image_url($qrcode);
  1009. }
  1010. /**
  1011. * 获取微信二维码数据
  1012. * @param $where 条件
  1013. * @param string $field 返回字段
  1014. * @return bool
  1015. * @throws \think\db\exception\DataNotFoundException
  1016. * @throws \think\db\exception\ModelNotFoundException
  1017. * @throws \think\exception\DbException
  1018. */
  1019. public static function getQrcode($where, $field = '')
  1020. {
  1021. if (!is_array($where)) {
  1022. return false;
  1023. }
  1024. $where['status'] = 1;
  1025. $field = $field ? $field : 'id,source_id,scene,ticket,url';
  1026. $info = db('qrcode')
  1027. ->where($where)
  1028. ->where('expire_at', '>', date('Y-m-d H:i:s'))
  1029. ->field($field)
  1030. ->find();
  1031. $ticket = isset($info['ticket']) ? $info['ticket'] : '';
  1032. if ($info && $ticket) {
  1033. $info['qrcode'] = sprintf(self::$apiUrl['getQrcodeByTicket'], $ticket);
  1034. }
  1035. return $info;
  1036. }
  1037. /**
  1038. * 响应消息
  1039. * @param $postObj
  1040. */
  1041. public static function responseText($postObj)
  1042. {
  1043. $openid = isset($postObj['FromUserName']) ? $postObj['FromUserName'] : '';
  1044. $msgId = isset($postObj['MsgId']) ? $postObj['MsgId'] : '';
  1045. $keyword = isset($postObj['Content']) ? trim($postObj['Content']) : '';
  1046. $cacheKey = "messages:replys:{$msgId}";
  1047. if(RedisService::get($cacheKey)){
  1048. return false;
  1049. }
  1050. WechatService::rebackOk();
  1051. echo ' ';
  1052. exit;
  1053. }
  1054. /**
  1055. * 响应消息
  1056. * @param $fromUsername 发送用户
  1057. * @param $toUsername 接收用户
  1058. * @param $contentStr 发送内容
  1059. * @param string $msgType 消息类型
  1060. */
  1061. public static function responseTplMsg($fromUsername, $toUsername, $contentStr, $msgType = 'text')
  1062. {
  1063. $textTpl = "<xml>
  1064. <ToUserName><![CDATA[%s]]></ToUserName>
  1065. <FromUserName><![CDATA[%s]]></FromUserName>
  1066. <CreateTime>%s</CreateTime>
  1067. <MsgType><![CDATA[%s]]></MsgType>
  1068. <Content><![CDATA[%s]]></Content>
  1069. </xml>";
  1070. $resultStr = sprintf($textTpl, $fromUsername, $toUsername, time(), $msgType, $contentStr);
  1071. echo $resultStr;
  1072. exit;
  1073. }
  1074. /**
  1075. * 创建菜单
  1076. * @param array $menus 菜单数组数据
  1077. * @params $delete 是否删除就菜单
  1078. * @return bool
  1079. */
  1080. public static function createMenu($menus = [], $delete = false)
  1081. {
  1082. $weixinConfig = config('weixin.');
  1083. $menus = $menus ? $menus : (isset($weixinConfig['menus']) ? $weixinConfig['menus'] : []);
  1084. if (empty($menus)) {
  1085. return false;
  1086. }
  1087. $tokenData = WechatService::getAccessToken('', 'accessToken');
  1088. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  1089. if (empty($token)) {
  1090. return false;
  1091. }
  1092. // 删除菜单
  1093. if ($delete) {
  1094. WechatService::delMenu();
  1095. }
  1096. $url = sprintf(self::$apiUrl['createMenu'], $token);
  1097. $result = httpRequest($url, json_encode(['button' => $menus], 256));
  1098. $errorCode = isset($result['errcode']) ? $result['errcode'] : true;
  1099. if ($errorCode != 0) {
  1100. return false;
  1101. }
  1102. return $result;
  1103. }
  1104. /**
  1105. * 删除菜单
  1106. * @return bool
  1107. */
  1108. public static function delMenu()
  1109. {
  1110. $tokenData = WechatService::getAccessToken('', 'accessToken');
  1111. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  1112. $url = sprintf(self::$apiUrl['delMenu'], $token);
  1113. $result = httpRequest($url);
  1114. $errorCode = isset($result['errcode']) ? $result['errcode'] : true;
  1115. if ($errorCode == 0) {
  1116. return false;
  1117. }
  1118. return true;
  1119. }
  1120. /**
  1121. * 查询菜单
  1122. * @return bool
  1123. */
  1124. public static function getMenu()
  1125. {
  1126. $tokenData = WechatService::getAccessToken('', 'accessToken');
  1127. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  1128. $url = sprintf(self::$apiUrl['getMenu'], $token);
  1129. $result = httpRequest($url);
  1130. return $result;
  1131. }
  1132. /**
  1133. * 发送客服消息
  1134. * @param $openid 接受用户OPENID
  1135. * @param $content 消息内容:数组
  1136. * @param string $msgType 消息类型
  1137. * @return mixed
  1138. */
  1139. public static function sendCustomMsg($openid, $content, $msgType = 'text')
  1140. {
  1141. $data = [
  1142. 'touser' => $openid,
  1143. 'msgtype' => $msgType,
  1144. $msgType => $content
  1145. ];
  1146. $lockKey = 'caches:weixin:custonLock:' . $openid . '_' . md5(json_encode($data));
  1147. if (RedisService::get($lockKey)) {
  1148. return false;
  1149. }
  1150. $tokenData = WechatService::getAccessToken('');
  1151. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  1152. RedisService::set($lockKey, $data, 5);
  1153. $url = sprintf(self::$apiUrl['customMessage'], $token);
  1154. $result = httpRequest($url, json_encode($data, 256));
  1155. RedisService::set('caches:weixin:customLock:' . $openid . '_' . date('YmdHis'), ['data' => $data, 'tokenData' => $tokenData, 'result' => $result], 3);
  1156. return $result;
  1157. }
  1158. /**
  1159. * 发送模板消息
  1160. * @param $openid OPENID
  1161. * @param $params 参数:title-标题(必填),type-模板类型标识字符串(必填),keywords-模板字段数据(必填),url-模板跳转链接,remark-模板备注信息
  1162. * @return array|int
  1163. */
  1164. public static function sendTplMsg($openid, $params, $formatUrl=true)
  1165. {
  1166. $title = isset($params['title']) ? $params['title'] : '';
  1167. $remark = isset($params['remark']) ? $params['remark'] : '';
  1168. $type = isset($params['type']) ? $params['type'] : 'default';
  1169. $keywords = isset($params['keywords']) ? $params['keywords'] : [];
  1170. $keywords = $keywords ? $keywords : [];
  1171. if ($title) {
  1172. $keywords['first'] = ['value' => $title, 'color' => '#173177'];
  1173. }
  1174. if ($remark) {
  1175. $keywords['remark'] = ['value' => $remark, 'color' => '#173177'];
  1176. }
  1177. $configService = new ConfigService();
  1178. $templates = $configService->getConfigByGroup(10);
  1179. $templateId = isset($templates[$type]) ? trim($templates[$type]) : '';
  1180. if (empty($templateId)) {
  1181. return 2110;
  1182. }
  1183. ksort($keywords);
  1184. $tplData = [
  1185. 'touser' => $openid,
  1186. 'template_id' => $templateId,
  1187. 'data' => $keywords,
  1188. ];
  1189. $url = isset($params['url']) ? trim($params['url']) : '';
  1190. if ($url) {
  1191. $tplData['url'] = $formatUrl? WechatService::makeRedirectUrl($url) : $url;
  1192. }
  1193. // 删除旧数据,新增消息记录
  1194. $tokenData = WechatService::getAccessToken('');
  1195. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  1196. if (empty($token)) {
  1197. return 1010;
  1198. }
  1199. $url = sprintf(self::$apiUrl['tplMessage'], $token);
  1200. $result = httpRequest($url, json_encode($tplData, 256));
  1201. $code = isset($result['errcode']) ? $result['errcode'] : true;
  1202. RedisService::set('caches:messages:result:'.$openid.'_'.date('YmdHi'), ['url'=> $url,'params'=> $tplData,'result'=> $result], 600);
  1203. if ($code == 'ok') {
  1204. return true;
  1205. } else {
  1206. $result = httpRequest($url, json_encode($tplData, 256));
  1207. $code = isset($result['errcode']) ? $result['errcode'] : true;
  1208. RedisService::set('caches:messages:result:'.$openid.'_'.date('YmdHi'), ['url'=> $url,'params'=> $tplData,'result'=> $result], 600);
  1209. if ($code === 0) {
  1210. return true;
  1211. }
  1212. }
  1213. return 2113;
  1214. }
  1215. /**
  1216. * 获取消息模板列表
  1217. * @return int|mixed
  1218. */
  1219. public static function getTemplateList(){
  1220. $tokenData = WechatService::getAccessToken('');
  1221. $token = isset($tokenData['token']) ? trim($tokenData['token']) : '';
  1222. if (empty($token)) {
  1223. return 1010;
  1224. }
  1225. $url = sprintf(self::$apiUrl['templateList'], $token);
  1226. $result = httpRequest($url);
  1227. RedisService::set('caches:messages:templates', $result, 600);
  1228. return $result;
  1229. }
  1230. /**
  1231. * 获取支付签名token
  1232. * @return string
  1233. */
  1234. public static function getSignToken($params, $type=1){
  1235. $url = $params['url'];
  1236. $url_parts = parse_url($url);
  1237. $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
  1238. $nonce = WechatService::createNonceStr(32);
  1239. $timestamp = time();
  1240. $body = isset($params['body'])? $params['body'] : '';
  1241. $method = isset($params['method'])? $params['method'] : 'POST';
  1242. $message = "{$method}\n".
  1243. $canonical_url."\n".
  1244. $timestamp."\n".
  1245. $nonce."\n";
  1246. if($body || $type == 2){
  1247. $message .= $body."\n";
  1248. }
  1249. $mch_private_key = openssl_get_privatekey(file_get_contents(self::$certPaths['key_path']));
  1250. openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
  1251. $sign = base64_encode($raw_sign);
  1252. $schema = 'WECHATPAY2-SHA256-RSA2048';
  1253. $serial_no = WechatService::getConfigs('wx_mch_cert_no');
  1254. $mchId = isset($params['mchid'])? $params['mchid'] : WechatService::getConfigs('wx_mchid');
  1255. $token = sprintf('%s mchid="%s",nonce_str="%s",timestamp="%d",signature="%s",serial_no="%s"', $schema, $mchId, $nonce, $timestamp, $sign, $serial_no);
  1256. return $token;
  1257. }
  1258. /**
  1259. * 获取支付证书列表
  1260. * @return array|mixed
  1261. */
  1262. public static function getCerts(){
  1263. $url = self::$apiUrl['getCerts'];
  1264. $mchId = WechatService::getConfigs('wx_mch_id');
  1265. $cacheKey = "caches:certs:{$mchId}:list";
  1266. $certs = RedisService::get($cacheKey);
  1267. if($certs){
  1268. return $certs;
  1269. }
  1270. $params = ['url'=> $url, 'method'=> 'GET','mchid'=> $mchId, 'body'=> ''];
  1271. $token = WechatService::getSignToken($params, 2);
  1272. RedisService::set('caches:certs:'.$mchId.':token', ['params'=>$params,'token'=> $token], 600);
  1273. $headers = ["Authorization: {$token}","Content-Type: application/json","Accept: application/json","User-Agent: https://zh.wikipedia.org/wiki/User_agent"];
  1274. RedisService::set('caches:certs:'.$mchId.':request', ['params'=>$params,'token'=> $token,'headers'=> $headers], 600);
  1275. $response = WechatService::curlPost($url, '',[],[],$headers);
  1276. $response = $response? json_decode($response, true) : $response;
  1277. RedisService::set('caches:certs:'.$mchId.':result', ['params'=>$params,'result'=> $response], 600);
  1278. if($response){
  1279. $certs = [];
  1280. foreach($response as $item){
  1281. $certs[$item['serial_no']] = $item;
  1282. }
  1283. if($certs){
  1284. RedisService::set($cacheKey, $certs, 300);
  1285. }
  1286. }
  1287. return $certs;
  1288. }
  1289. /**
  1290. * Read certificate from file
  1291. *
  1292. * @param string $filepath PEM encoded X.509 certificate file path
  1293. *
  1294. * @return resource|bool X.509 certificate resource identifier on success or FALSE on failure
  1295. */
  1296. public static function getCertificate() {
  1297. return openssl_x509_read(file_get_contents(self::$certPaths['cert_path']));}
  1298. /**
  1299. * 生成普通参数二维码
  1300. * @param $str 参数
  1301. * @param bool $refresh 是否重新生成
  1302. * @return bool
  1303. */
  1304. public static function makeNormalQrcode($str, $refresh = false, $size = 8, $margin=2)
  1305. {
  1306. $qrFile = '/upload/qrcode/member/';
  1307. if (!is_dir($qrFile)) {
  1308. @mkdir('.' . $qrFile, 0755, true);
  1309. }
  1310. $qrFile = $qrFile . 'U_' . strtoupper(md5($str . '_' . $size)) . '.jpg';
  1311. if (is_file($qrFile) && !$refresh) {
  1312. return false;
  1313. }
  1314. $renderer = new ImageRenderer(
  1315. new RendererStyle(360),
  1316. new ImagickImageBackEnd()
  1317. );
  1318. $writer = new Writer($renderer);
  1319. $writer->writeFile($str, $qrFile);
  1320. if(!file_exists('.'.$qrFile)){
  1321. return false;
  1322. }
  1323. return $qrFile;
  1324. }
  1325. /**
  1326. * 返回给微信
  1327. */
  1328. public static function rebackOk(){
  1329. echo '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
  1330. exit;
  1331. }
  1332. }
  1333. ?>