WeChatServiceImpl.java 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. package com.cyksj.service.wechat.impl;
  2. import cn.hutool.core.date.DateTime;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.util.CharsetUtil;
  5. import cn.hutool.core.util.RandomUtil;
  6. import cn.hutool.core.util.StrUtil;
  7. import cn.hutool.http.ssl.SSLSocketFactoryBuilder;
  8. import cn.hutool.json.JSONObject;
  9. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  10. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  11. import com.cyksj.common.constant.Constant;
  12. import com.cyksj.common.exception.BusinessRuntimeException;
  13. import com.cyksj.common.util.*;
  14. import com.cyksj.config.BaseWeChatConfig;
  15. import com.cyksj.config.WeChatConfig;
  16. import com.cyksj.config.WeChatFactory;
  17. import com.cyksj.config.YhCertMiniConfig;
  18. import com.cyksj.enums.WeChatAuthRedirectUrl;
  19. import com.cyksj.mapper.WxAppMapper;
  20. import com.cyksj.mapper.channel.ShopConfigMapper;
  21. import com.cyksj.model.dto.*;
  22. import com.cyksj.model.entity.ShopConfig;
  23. import com.cyksj.model.entity.SysConfig;
  24. import com.cyksj.model.entity.WxApp;
  25. import com.cyksj.model.request.TargetAppletSchemeReq;
  26. import com.cyksj.model.response.WxGzhQrCodeTicketRep;
  27. import com.cyksj.model.wechat.MiniURLLink;
  28. import com.cyksj.redis.RedisService;
  29. import com.cyksj.service.sys.SysConfigService;
  30. import com.cyksj.service.wechat.WeChatService;
  31. import com.cyksj.service.wechat.WxMpAccountOps;
  32. import lombok.RequiredArgsConstructor;
  33. import lombok.extern.slf4j.Slf4j;
  34. import org.apache.commons.lang3.StringUtils;
  35. import org.apache.logging.log4j.util.Strings;
  36. import org.springframework.stereotype.Service;
  37. import javax.crypto.Mac;
  38. import javax.crypto.spec.SecretKeySpec;
  39. import javax.net.ssl.KeyManagerFactory;
  40. import java.io.File;
  41. import java.io.FileInputStream;
  42. import java.io.InputStream;
  43. import java.math.BigDecimal;
  44. import java.net.URLEncoder;
  45. import java.net.http.HttpRequest;
  46. import java.net.http.HttpResponse;
  47. import java.security.KeyStore;
  48. import java.util.*;
  49. /**
  50. * @author chan
  51. * @description
  52. * @ClassName WeChatServiceImpl
  53. * @date 2020-08-05 11:00 上午
  54. */
  55. @Slf4j
  56. @Service
  57. @RequiredArgsConstructor
  58. public class WeChatServiceImpl implements WeChatService {
  59. private final WeChatConfig weChatConfig;
  60. private final WxMpAccountOps wxMpAccountOps;
  61. private final WxAppMapper wxAppMapper;
  62. private final ShopConfigMapper shopConfigMapper;
  63. private final RedisService redisService;
  64. private final SysConfigService sysConfigService;
  65. private final YhCertMiniConfig certMiniConfig;
  66. @Override
  67. public String createUrl(String state, String redirectUrl) {
  68. return createUrl(state,redirectUrl,weChatConfig.getAppid(), WeChatAuthRedirectUrl.BASE.getScope());
  69. }
  70. @Override
  71. public String createUrl(String state, String redirectUrl,String appId,String scope) {
  72. return "https://open.weixin.qq.com/connect/oauth2/authorize" +
  73. "?appid=" + appId +
  74. "&redirect_uri=" + URLEncoder.encode(redirectUrl, IoKit.Charsets.UTF_8.getCharset()) +
  75. "&response_type=code" +
  76. "&scope=" + scope +
  77. "&state=" + state + "#wechat_redirect";
  78. }
  79. @Override
  80. public AuthToken getAuthToken(String code) throws Exception {
  81. return getAuthToken(code, weChatConfig.getAppid());
  82. }
  83. public String getSecret(String appId){
  84. WxApp wxApp = wxAppMapper.selectOne(new QueryWrapper<WxApp>().lambda().eq(WxApp::getAppId, appId));
  85. if (Optional.ofNullable(wxApp).isPresent()) {
  86. return wxApp.getAppSecret();
  87. }else {
  88. return Strings.EMPTY;
  89. }
  90. }
  91. @Override
  92. public AuthToken getAuthToken(String code,String appId) throws Exception {
  93. String secret = getSecret(appId);
  94. if(StringUtils.isBlank(secret)){
  95. throw BusinessRuntimeException.getInstance("授权公众号不存在!");
  96. }
  97. String url = "https://api.weixin.qq.com/sns/oauth2/access_token" +
  98. "?appid=" + appId +
  99. "&secret=" + secret +
  100. "&code=" + code +
  101. "&grant_type=authorization_code";
  102. HttpResponse<byte[]> res =
  103. J11HttpC.custom()
  104. .ofGet()
  105. .url(url)
  106. .send(HttpResponse.BodyHandlers.ofByteArray());
  107. if (200 != res.statusCode()) {
  108. throw BusinessRuntimeException.getInstance("请求user_token失败: " + IoKit.toString(res.body()));
  109. }
  110. return Jsons.parseObject(res.body(), AuthToken.class);
  111. }
  112. @Override
  113. public GzhOAuth2UserInfo getUserInfo(String accessToken, String openid) throws Exception {
  114. String url = "https://api.weixin.qq.com/sns/userinfo" +
  115. "?access_token=" + accessToken +
  116. "&openid=" + openid +
  117. "&lang=zh_CN";
  118. HttpResponse<byte[]> res =
  119. J11HttpC.custom()
  120. .ofGet()
  121. .url(url)
  122. .send(HttpResponse.BodyHandlers.ofByteArray());
  123. if (200 != res.statusCode()) {
  124. throw BusinessRuntimeException.getInstance("请求userinfo失败: " + IoKit.toString(res.body()));
  125. }
  126. return Jsons.parseObject(res.body(), GzhOAuth2UserInfo.class);
  127. }
  128. @Override
  129. public GzhUnionidUserinfo getUserInfo2(String accessToken, String openid) throws Exception {
  130. String url = "https://api.weixin.qq.com/cgi-bin/user/info" +
  131. "?access_token=" + accessToken +
  132. "&openid=" + openid +
  133. "&lang=zh_CN";
  134. HttpResponse<byte[]> res =
  135. J11HttpC.custom()
  136. .ofGet()
  137. .url(url)
  138. .send(HttpResponse.BodyHandlers.ofByteArray());
  139. if (200 != res.statusCode()) {
  140. throw BusinessRuntimeException.getInstance("请求userinfo失败: " + IoKit.toString(res.body()));
  141. }
  142. GzhUnionidUserinfo info = Jsons.parseObject(res.body(), GzhUnionidUserinfo.class);
  143. if (null == info.getSubscribe() || info.getSubscribe() == 0) {
  144. return null;
  145. }
  146. return info;
  147. }
  148. @Override
  149. public String getJsTicket() throws Exception {
  150. return getJsTicket(null);
  151. }
  152. @Override
  153. public String getJsTicket(String appId) throws Exception {
  154. appId = Optional.ofNullable(appId).orElse(weChatConfig.getAppid());
  155. String jsTicket = wxMpAccountOps.jsTicket(appId);
  156. if (StringUtils.isBlank(jsTicket)) {
  157. throw BusinessRuntimeException.getInstance("获取用户信息失败");
  158. }
  159. return jsTicket;
  160. }
  161. @Override
  162. public String getAccessToken() throws Exception {
  163. return getAccessToken(null);
  164. }
  165. @Override
  166. public String getAccessToken(String appId) throws Exception {
  167. appId = Optional.ofNullable(appId).orElse(weChatConfig.getAppid());
  168. String accessToken = wxMpAccountOps.accessToken(appId);
  169. log.info("------------------accessToken:{} \n",accessToken);
  170. if (StringUtils.isBlank(accessToken)) {
  171. throw BusinessRuntimeException.getInstance("获取用户信息失败:");
  172. }
  173. return accessToken;
  174. }
  175. @Override
  176. public H5JsPayParams getJsPayParams(String appid, String shopId, String openid, Integer totalFee, String outTradeNo, String notifyUrl, String attach, String body, String tradeType, Date expireTime) throws Exception {
  177. log.info("------------------- 调用微信h5支付 开始 -------------------");
  178. ShopConfig shopConfig = shopConfigMapper.selectOne(Wrappers.lambdaQuery(ShopConfig.class).eq(ShopConfig::getAppId, shopId).last(" limit 1"));
  179. // 拼接统一下单实体类
  180. UnifiedOrder order = new UnifiedOrder()
  181. .setAppid(appid)
  182. .setAttach(attach)
  183. .setBody(body)
  184. .setMchId(shopConfig.getAppId())
  185. .setNonceStr("chan.123")
  186. .setNotifyUrl(notifyUrl)
  187. .setOutTradeNo(outTradeNo)
  188. .setSpbillCreateIp("127.0.0.1")
  189. .setTotalFee(totalFee)
  190. .setTradeType(tradeType);
  191. if (expireTime != null) {
  192. //5分钟过期
  193. order.setTimeExpire(DateUtil.format(DateUtil.offsetMinute(expireTime, 5), "yyyyMMddHHmmss"));
  194. }
  195. if (!"NATIVE".equals(tradeType)) {
  196. order.setOpenid(openid);
  197. }
  198. String sign = Codec.DoDigest.custom()
  199. .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  200. .setStringData(order.getPrintln(tradeType, shopConfig.getSecret()))
  201. .toHexString()
  202. .toUpperCase();
  203. order.setSign(sign);
  204. // xml
  205. String xml = Xmls.toXml(order);
  206. log.info("调用微信h5支付, 请求参数: \n{}", xml);
  207. HttpResponse<byte[]> res =
  208. J11HttpC.custom()
  209. .ofPost()
  210. .url("https://api.mch.weixin.qq.com/pay/unifiedorder")
  211. .cacheURI()
  212. .headers(J11HttpC.ReqType.raw_xml)
  213. .body(HttpRequest.BodyPublishers.ofString(xml, IoKit.Charsets.UTF_8.getCharset()))
  214. .send(HttpResponse.BodyHandlers.ofByteArray());
  215. if (200 != res.statusCode()) {
  216. throw BusinessRuntimeException.getInstance("请求wx_prepay失败: " + IoKit.toString(res.body()));
  217. }
  218. Map<String, String> wxResult = Xmls.toMap(res.body());
  219. log.info("微信返回值 xml转map: \n{}", wxResult);
  220. if (!StringUtils.equals(wxResult.get("return_code"), "SUCCESS") || !StringUtils.equals(wxResult.get("result_code"), "SUCCESS")) {
  221. throw BusinessRuntimeException.getInstance("调用微信h5支付失败. 微信返回: " + wxResult);
  222. }
  223. if (StringUtils.equals(wxResult.get("return_code"), "SUCCESS") && !StringUtils.equals(wxResult.get("result_code"), "SUCCESS")) {
  224. throw BusinessRuntimeException.getInstance("订单异常, 原因: " + wxResult.get("err_code") + "[" + wxResult.get("err_code_des") + "]");
  225. }
  226. if (StringUtils.isBlank(wxResult.get("prepay_id"))) {
  227. throw BusinessRuntimeException.getInstance("无法获取prepay_id.");
  228. }
  229. // 校验签名
  230. boolean f = this.checkSignWithMd5(wxResult,shopConfig.getSecret());
  231. if (!f) {
  232. throw BusinessRuntimeException.getInstance("调用微信h5支付, 微信返回值的签名验证失败.");
  233. }
  234. H5JsPayParams params = new H5PayParams();
  235. params.setAppId(appid);
  236. params.setNonceStr(order.getNonceStr());
  237. params.setSignType("MD5");
  238. params.setTimestamp(System.currentTimeMillis());
  239. params.setPrepayId("prepay_id=" + wxResult.get("prepay_id"));
  240. sign = Codec.DoDigest.custom()
  241. .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  242. .setStringData(params.println(shopConfig.getSecret()))
  243. .toHexString()
  244. .toUpperCase();
  245. params.setPaySign(sign);
  246. //二维码链接
  247. params.setCodeUrl(wxResult.get("code_url"));
  248. log.info("------------------- 调用微信h5支付 结束 -------------------");
  249. return params;
  250. }
  251. @Override
  252. public boolean checkSignWithMd5(Map<String, String> map) throws Exception {
  253. return checkSignWithMd5(map,weChatConfig.getShopKey());
  254. }
  255. @Override
  256. public boolean checkSignWithMd5(Map<String, String> map,String shopKey) throws Exception {
  257. // 将map的key按ASCii升序排列
  258. String[] keys = map.keySet().toArray(new String[0]);
  259. Arrays.sort(keys);
  260. // 将参数有序排列
  261. StringBuilder str = new StringBuilder();
  262. for (int i = 0; i < keys.length; i++) {
  263. if (StringUtils.equals(keys[i], "sign")) {
  264. continue;
  265. }
  266. if (StringUtils.isBlank(map.get(keys[i]))) {
  267. continue;
  268. }
  269. str.append(keys[i]).append("=").append(map.get(keys[i])).append("&");
  270. }
  271. str.append("key=").append(shopKey);
  272. log.info("签名验证, 排列好的有序字符串: {}", str.toString());
  273. String sign = Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.MD5).setStringData(str.toString()).toHexString().toUpperCase();
  274. return StringUtils.equals(map.get("sign"), sign);
  275. }
  276. @Override
  277. public H5PayParams getH5PayParams(String appid, Integer totalFee, String outTradeNo, String notifyUrl, String attach, String ip,String body) throws Exception {
  278. log.info("------------------- 调用微信h5支付 开始 -------------------");
  279. BaseWeChatConfig weChatConfigStrategy = WeChatFactory.getWeChatConfigStrategy(appid);
  280. // 拼接统一下单实体类
  281. UnifiedOrder order = new UnifiedOrder();
  282. order.setAppid(appid);
  283. order.setAttach(attach);
  284. order.setBody(body);
  285. order.setMchId(weChatConfigStrategy.getShopid());
  286. order.setNonceStr("chan123");
  287. order.setNotifyUrl(notifyUrl);
  288. order.setOutTradeNo(outTradeNo);
  289. order.setSpbillCreateIp(ip);
  290. order.setTotalFee(totalFee);
  291. order.setTradeType("MWEB");
  292. String sceneInfo= "{\"h5_info\": {\"type\":\"Wap\",\"wap_url\": \"" + weChatConfigStrategy.getDomain() +"\",\"wap_name\": \"善行\"}}";
  293. order.setSceneInfo(sceneInfo);
  294. // order.setSignType("MD5");
  295. String sign = Codec.DoDigest.custom()
  296. .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  297. .setStringData(order.H5Println(weChatConfigStrategy.getShopKey()))
  298. .toHexString()
  299. .toUpperCase();
  300. order.setSign(sign);
  301. // xml
  302. String xml = Xmls.toXml(order);
  303. log.info("调用微信h5支付, 请求参数: \n{}", xml);
  304. HttpResponse<byte[]> res =
  305. J11HttpC.custom()
  306. .ofPost()
  307. .url("https://api.mch.weixin.qq.com/pay/unifiedorder")
  308. .cacheURI()
  309. .headers(J11HttpC.ReqType.raw_xml)
  310. .body(HttpRequest.BodyPublishers.ofString(xml, IoKit.Charsets.UTF_8.getCharset()))
  311. .send(HttpResponse.BodyHandlers.ofByteArray());
  312. if (200 != res.statusCode()) {
  313. throw BusinessRuntimeException.getInstance("请求wx_prepay失败: " + IoKit.toString(res.body()));
  314. }
  315. Map<String, String> wxResult = Xmls.toMap(res.body());
  316. log.info("微信返回值 xml转map: \n{}", wxResult);
  317. if (!StringUtils.equals(wxResult.get("return_code"), "SUCCESS") || !StringUtils.equals(wxResult.get("result_code"), "SUCCESS")) {
  318. throw BusinessRuntimeException.getInstance("调用微信h5支付失败. 微信返回: " + wxResult);
  319. }
  320. if (StringUtils.equals(wxResult.get("return_code"), "SUCCESS") && !StringUtils.equals(wxResult.get("result_code"), "SUCCESS")) {
  321. throw BusinessRuntimeException.getInstance("订单异常, 原因: " + wxResult.get("err_code") + "[" + wxResult.get("err_code_des") + "]");
  322. }
  323. if (StringUtils.isBlank(wxResult.get("prepay_id"))) {
  324. throw BusinessRuntimeException.getInstance("无法获取prepay_id.");
  325. }
  326. // 校验签名
  327. boolean f = this.checkSignWithMd5(wxResult,weChatConfigStrategy.getShopKey());
  328. if (!f) {
  329. throw BusinessRuntimeException.getInstance("调用微信h5支付, 微信返回值的签名验证失败.");
  330. }
  331. H5PayParams params = new H5PayParams();
  332. params.setAppId(appid);
  333. params.setNonceStr(order.getNonceStr());
  334. params.setSignType("MD5");
  335. params.setTimestamp(System.currentTimeMillis());
  336. params.setPrepayId("prepay_id=" + wxResult.get("prepay_id"));
  337. params.setMWebUrl(wxResult.get("mweb_url"));
  338. sign = Codec.DoDigest.custom()
  339. .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  340. .setStringData(params.println(weChatConfigStrategy.getShopKey()))
  341. .toHexString()
  342. .toUpperCase();
  343. params.setPaySign(sign);
  344. log.info("------------------- 调用微信h5支付 结束 -------------------");
  345. return params;
  346. }
  347. @Override
  348. public void sendTemplateMessage(String accessToken, String json) throws Exception{
  349. String url = "https://api.weixin.qq.com/cgi-bin/message/template/send" +
  350. "?access_token=" + accessToken;
  351. HttpResponse<String> send = J11HttpC.custom()
  352. .ofPost()
  353. .url(url)
  354. .headers(J11HttpC.ReqType.raw_json)
  355. .body(HttpRequest.BodyPublishers.ofString(json))
  356. .send(HttpResponse.BodyHandlers.ofString());
  357. log.info("发送模板消息结束,返回值:{}", send != null ? send.body() : null);
  358. }
  359. @Override
  360. public void refundWxOrder(String appId,String shopId, String transactionId, String outRefundNo, BigDecimal totalFee, BigDecimal refundMoney, String notifyUrl) throws Exception {
  361. ShopConfig shopConfig = shopConfigMapper.selectOne(Wrappers.lambdaQuery(ShopConfig.class).eq(ShopConfig::getAppId, shopId).last(" limit 1"));
  362. WxRefundOrder wxRefundOrder = new WxRefundOrder();
  363. wxRefundOrder.setAppid(appId);
  364. //商户号
  365. wxRefundOrder.setMchId(shopConfig.getAppId());
  366. wxRefundOrder.setOutRefundNo(outRefundNo);
  367. wxRefundOrder.setNonceStr("chan123");
  368. wxRefundOrder.setTotalFee(totalFee.intValue());
  369. //退款金额
  370. wxRefundOrder.setRefundFee(refundMoney.intValue());
  371. wxRefundOrder.setTransactionId(transactionId);
  372. // wxRefundOrder.setRefundDesc("订单退款");
  373. wxRefundOrder.setNotifyUrl(notifyUrl);
  374. String sign = Codec.DoDigest.custom()
  375. .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  376. .setStringData(wxRefundOrder.println(shopConfig.getSecret()))
  377. .toHexString()
  378. .toUpperCase();
  379. wxRefundOrder.setSign(sign);
  380. String xml = Xmls.toXml(wxRefundOrder);
  381. KeyStore keyStore = KeyStore.getInstance("PKCS12");
  382. File file = new File("cert/" + shopId + ".p12");
  383. InputStream inputStream = new FileInputStream(file);
  384. keyStore.load(inputStream, shopId.toCharArray());
  385. // 初始化密钥库
  386. KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
  387. factory.init(keyStore, shopId.toCharArray());
  388. cn.hutool.http.HttpRequest request = cn.hutool.http.HttpRequest.post("https://api.mch.weixin.qq.com/secapi/pay/refund")
  389. // 自定义返回编码
  390. .charset(CharsetUtil.CHARSET_UTF_8)
  391. // 禁用缓存
  392. .disableCache()
  393. .setSSLSocketFactory(SSLSocketFactoryBuilder.create()
  394. .setKeyManagers(factory.getKeyManagers())
  395. .setProtocol(SSLSocketFactoryBuilder.TLSv1).build());
  396. cn.hutool.http.HttpResponse res = request.body(xml).execute();
  397. if (200 != res.getStatus()) {
  398. throw BusinessRuntimeException.getInstance("请求wx_refund失败: " + res.body());
  399. }
  400. Map<String, String> wxRefund = Xmls.toMap(res.body());
  401. log.info("微信返回值 xml 转 map: \n{}", wxRefund);
  402. if (!StringUtil.equals(wxRefund.get("return_code"), "SUCCESS") || !StringUtil.equals(wxRefund.get("result_code"), "SUCCESS")) {
  403. throw BusinessRuntimeException.getInstance("调用微信申请退款失败. 微信返回: " + wxRefund);
  404. }
  405. if (StringUtil.equals(wxRefund.get("return_code"), "SUCCESS") && !StringUtil.equals(wxRefund.get("result_code"), "SUCCESS")) {
  406. throw BusinessRuntimeException.getInstance("订单异常: " + wxRefund.get("err_code") + "[" + wxRefund.get("err_code_des") + "]");
  407. }
  408. // 校验签名
  409. boolean f = this.checkSignWithMd5(wxRefund, shopConfig.getSecret());
  410. if (!f) {
  411. throw BusinessRuntimeException.getInstance("调用微信h5申请退款, 微信返回值的签名验证失败.");
  412. }
  413. log.info("------------------- 调用微信申请退款 结束 -------------------");
  414. }
  415. @Override
  416. public Map<String, String> getWxRefundDetail(String appId, String shopId, String outRefundNo) throws Exception {
  417. ShopConfig shopConfig = shopConfigMapper.selectOne(Wrappers.lambdaQuery(ShopConfig.class)
  418. .eq(ShopConfig::getAppId, shopId).last("limit 1"));
  419. if (shopConfig == null || StrUtil.isBlank(shopConfig.getSecret())) {
  420. throw BusinessRuntimeException.getInstance("微信退款查询商户不存在");
  421. }
  422. Map<String, String> requestData = new TreeMap<>();
  423. requestData.put("appid", appId);
  424. requestData.put("mch_id", shopId);
  425. requestData.put("out_refund_no", outRefundNo);
  426. requestData.put("nonce_str", UUID.randomUUID().toString().replace("-", ""));
  427. requestData.put("sign", signWithMd5(requestData, shopConfig.getSecret()));
  428. HttpResponse<String> response = J11HttpC.custom()
  429. .ofPost()
  430. .url("https://api.mch.weixin.qq.com/pay/refundquery")
  431. .headers(J11HttpC.ReqType.raw_xml)
  432. .body(HttpRequest.BodyPublishers.ofString(Xmls.toXml(requestData), IoKit.Charsets.UTF_8.getCharset()))
  433. .send(HttpResponse.BodyHandlers.ofString());
  434. if (response.statusCode() != 200) {
  435. throw BusinessRuntimeException.getInstance("微信退款查询失败: " + response.body());
  436. }
  437. Map<String, String> result = Xmls.toMap(response.body());
  438. if (!"SUCCESS".equals(result.get("return_code"))) {
  439. throw BusinessRuntimeException.getInstance("微信退款查询通信失败: " + result);
  440. }
  441. if (StrUtil.isNotBlank(result.get("mch_id")) && !shopId.equals(result.get("mch_id"))) {
  442. throw BusinessRuntimeException.getInstance("微信退款查询商户不匹配");
  443. }
  444. if (StrUtil.isNotBlank(result.get("appid")) && !appId.equals(result.get("appid"))) {
  445. throw BusinessRuntimeException.getInstance("微信退款查询应用不匹配");
  446. }
  447. if (!"SUCCESS".equals(result.get("result_code"))) {
  448. return result;
  449. }
  450. if (!checkSignWithMd5(result, shopConfig.getSecret())) {
  451. throw BusinessRuntimeException.getInstance("微信退款查询签名校验失败");
  452. }
  453. return result;
  454. }
  455. private String signWithMd5(Map<String, String> values, String secret) throws Exception {
  456. StringBuilder data = new StringBuilder();
  457. values.entrySet().stream()
  458. .filter(entry -> !"sign".equals(entry.getKey()) && StrUtil.isNotBlank(entry.getValue()))
  459. .sorted(Map.Entry.comparingByKey())
  460. .forEach(entry -> data.append(entry.getKey()).append('=').append(entry.getValue()).append('&'));
  461. data.append("key=").append(secret);
  462. return Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  463. .setStringData(data.toString()).toHexString().toUpperCase();
  464. }
  465. @Override
  466. public WxGzhQrCodeTicketRep getWxGzhQrCode(GzhBodyDto gzhBodyDto) throws Exception {
  467. SysConfig sysConfig = sysConfigService.getOne(Wrappers.lambdaQuery(SysConfig.class)
  468. .eq(SysConfig::getSysKey, Constant.QR_CODE_WX_APP_ID).last("limit 1"));
  469. //pc公众号扫码
  470. String appid = "wx30f48135b1fd96bb";
  471. if (sysConfig != null) {
  472. String sysValue = sysConfig.getSysValue();
  473. if (StrUtil.isNotBlank(sysValue)) {
  474. appid = sysValue;
  475. }
  476. }
  477. String loginPhone = gzhBodyDto.getLoginPhone();
  478. String loginEmail = gzhBodyDto.getLoginEmail();
  479. String gzhQrCodeUrl = String.format("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", this.getAccessToken(appid));
  480. //scene_str:场景值ID(字符串形式的ID),字符串类型,长度限制为1到64
  481. String scene_str = RandomUtil.randomString(16);
  482. int expireTime = 60;
  483. if (StrUtil.isNotBlank(loginPhone) || StrUtil.isNotBlank(loginEmail)) {
  484. expireTime = 25;
  485. }
  486. DateTime dateTime = DateUtil.offsetSecond(DateTime.now(), expireTime);
  487. String body = String.format("{\"expire_seconds\": %s, \"action_name\": \"%s\", \"action_info\": {\"scene\": {\"scene_str\": \"%s\"}}}",
  488. expireTime,
  489. "QR_STR_SCENE",
  490. scene_str);
  491. HttpResponse<String> response = J11HttpC.custom()
  492. .url(gzhQrCodeUrl)
  493. .ofPost()
  494. .body(HttpRequest.BodyPublishers.ofString(body))
  495. .send(HttpResponse.BodyHandlers.ofString());
  496. if (200 != response.statusCode()) {
  497. throw BusinessRuntimeException.getInstance("获取微信登录二维码失败: " + response.body());
  498. }
  499. String state = Codec.DoBase64.custom().setData(Jsons.toJson(gzhBodyDto)).encodeAsText();
  500. redisService.set(RedisService.key.WX_GZH_QRCODE_SHARED_LOGIN.getNameFormat(scene_str), state, RedisService.key.WX_GZH_QRCODE_SHARED_LOGIN.getTimeout());
  501. JSONObject re = Jsons.parseObject(response.body(), JSONObject.class);
  502. String ticket = re.getStr("ticket");
  503. WxGzhQrCodeTicketRep wxGzhQrCodeTicketRep = new WxGzhQrCodeTicketRep();
  504. wxGzhQrCodeTicketRep.setQrCodeUrl(String.format("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s", ticket));
  505. wxGzhQrCodeTicketRep.setSceneStr(scene_str);
  506. wxGzhQrCodeTicketRep.setExpireTime(dateTime.getTime());
  507. return wxGzhQrCodeTicketRep;
  508. }
  509. @Override
  510. public String getNotExpiredWxGzhQrCode(String sceneKey) throws Exception {
  511. String gzhQrCodeUrl = String.format("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s", this.getAccessToken());
  512. String body = String.format("{\"action_name\": \"%s\", \"action_info\": {\"scene\": {\"scene_str\": \"%s\"}}}",
  513. "QR_LIMIT_STR_SCENE",
  514. sceneKey);
  515. HttpResponse<String> response = J11HttpC.custom()
  516. .url(gzhQrCodeUrl)
  517. .ofPost()
  518. .body(HttpRequest.BodyPublishers.ofString(body))
  519. .send(HttpResponse.BodyHandlers.ofString());
  520. if (200 != response.statusCode()) {
  521. throw BusinessRuntimeException.getInstance("获取永久渠道二维码失败: " + response.body());
  522. }
  523. JSONObject re = Jsons.parseObject(response.body(), JSONObject.class);
  524. String ticket = re.getStr("ticket");
  525. String url = String.format("https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s", ticket);
  526. return url;
  527. }
  528. @Override
  529. public Map<String, String> getWxOrderDetail(String shopId, String orderNo) throws Exception {
  530. String url = "https://api.mch.weixin.qq.com/pay/orderquery";
  531. // 拼接统一下单实体类
  532. Map<String, String> map = new TreeMap<>();
  533. map.put("appid", weChatConfig.getAppid());
  534. map.put("mch_id", shopId);
  535. map.put("out_trade_no", orderNo);
  536. map.put("nonce_str", "d1234");
  537. String sign = getMd5Sign(map);
  538. map.put("sign", sign);
  539. String xml = Xmls.toXml(map);
  540. HttpResponse<String> send = J11HttpC.custom()
  541. .ofPost()
  542. .url(url)
  543. .headers(J11HttpC.ReqType.raw_xml)
  544. .body(HttpRequest.BodyPublishers.ofString(xml, IoKit.Charsets.UTF_8.getCharset()))
  545. .send(HttpResponse.BodyHandlers.ofString());
  546. Map<String, String> orderSearch = Xmls.toMap(send.body());
  547. ShopConfig shopConfig = shopConfigMapper.getByAppId(shopId);
  548. if (shopConfig == null || !checkSignWithMd5(orderSearch, shopConfig.getSecret())) {
  549. throw BusinessRuntimeException.getInstance("微信订单查询签名校验失败");
  550. }
  551. log.info("微信返回值 xml转map: \n{}", orderSearch);
  552. if (!StringUtils.equals(orderSearch.get("result_code"), "SUCCESS")) {
  553. log.error("调用微信h5查询订单详情接口失败. 微信返回: " + orderSearch);
  554. // Preserve verified failure codes such as ORDERNOTEXIST for tri-state handling.
  555. return orderSearch;
  556. }
  557. return orderSearch;
  558. }
  559. @Override
  560. public String createWxAuthLoginUrl(String state, String appId, String redirectUri) {
  561. String authLoginUrl = String.format("https://open.weixin.qq.com/connect/qrconnect?" +
  562. "appid=%s&redirect_uri=%s&response_type=code&fast_login=0&scope=snsapi_login&state=%s#wechat_redirect", appId, redirectUri, state);
  563. return authLoginUrl;
  564. }
  565. @Override
  566. public String getMiniURLLink(String accessToken, MiniURLLink miniURLLink) throws Exception{
  567. String url = String.format("https://api.weixin.qq.com/wxa/generate_urllink?access_token=%s", accessToken);
  568. HttpResponse<String> send = J11HttpC.custom()
  569. .url(url)
  570. .ofPost()
  571. .body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(miniURLLink)))
  572. .send(HttpResponse.BodyHandlers.ofString());
  573. if (send.statusCode() != 200) {
  574. throw BusinessRuntimeException.getInstance("获取小程序链接错误");
  575. }
  576. JSONObject body = Jsons.parseObject(send.body(), JSONObject.class);
  577. if (body.getInt("errcode") != 0) {
  578. throw BusinessRuntimeException.getInstance(body.getStr("errmsg"));
  579. }
  580. return body.getStr("url_link");
  581. }
  582. /**
  583. * sha256_HMAC加密
  584. * @param message 消息
  585. * @param secret 秘钥
  586. * @return 加密后字符串
  587. */
  588. public static String sha256_HMAC(String message,String secret) {
  589. String hash = "";
  590. try {
  591. Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
  592. SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(IoKit.Charsets.UTF_8.getCharset()), "HmacSHA256");
  593. sha256_HMAC.init(secret_key);
  594. byte[] bytes = sha256_HMAC.doFinal(message.getBytes());
  595. hash = byteArrayToHexString(bytes);
  596. } catch (Exception e) {
  597. System.out.println("Error HmacSHA256 ===========" + e.getMessage());
  598. }
  599. return hash;
  600. }
  601. /**
  602. * 将加密后的字节数组转换成字符串
  603. *
  604. * @param b 字节数组
  605. * @return 字符串
  606. */
  607. public static String byteArrayToHexString(byte[] b) {
  608. StringBuilder hs = new StringBuilder();
  609. String stmp;
  610. for (int n = 0; b!=null && n < b.length; n++) {
  611. stmp = Integer.toHexString(b[n] & 0XFF);
  612. if (stmp.length() == 1) {
  613. hs.append('0');
  614. }
  615. hs.append(stmp);
  616. }
  617. return hs.toString().toLowerCase();
  618. }
  619. public String getMd5Sign(Map<String, String> map) throws Exception {
  620. StringBuilder sb = new StringBuilder();
  621. map.forEach((k, v) -> {
  622. sb.append(k).append("=").append(v).append("&");
  623. });
  624. sb.append("key").append("=").append(shopConfigMapper.getByAppId(map.get("mch_id")).getSecret());
  625. String sign = Codec.DoDigest.custom()
  626. .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
  627. .setStringData(sb.toString())
  628. .toHexString()
  629. .toUpperCase();
  630. return sign;
  631. }
  632. @Override
  633. public MiniProgramSession code2Session(String appId,String code) throws Exception {
  634. appId = Optional.ofNullable(appId).orElse(weChatConfig.getAppid());
  635. return wxMpAccountOps.miniProgramCode2Session(appId,code);
  636. }
  637. @Override
  638. public void getMiniStudentIdentity(String openId, String code) throws Exception {
  639. String url = "https://api.weixin.qq.com/intp/quickcheckstudentidentity?access_token=" + getAccessToken(certMiniConfig.getAppId());
  640. JSONObject params = new JSONObject();
  641. params.putOpt("openid", openId);
  642. params.putOpt("wx_studentcheck_code", code);
  643. HttpResponse<String> response = J11HttpC.custom().url(url).ofPost().body(HttpRequest.BodyPublishers.ofString(params.toString())).send(HttpResponse.BodyHandlers.ofString());
  644. JSONObject re = Jsons.parseObject(response.body(), JSONObject.class);
  645. if (re.getInt("errcode") != 0) {
  646. log.info("获取学生信息失败:{}", re.getStr("errmsg"));
  647. throw BusinessRuntimeException.getInstance("获取认证信息失败");
  648. }
  649. Integer bindStatus = Optional.ofNullable(re.getInt("bind_status")).orElse(1);
  650. //绑定状态:
  651. //1-未绑定
  652. //2-审核中
  653. //3-已绑定
  654. if (bindStatus == 1) {
  655. throw BusinessRuntimeException.getInstance("大学生身份未绑定");
  656. }
  657. if (bindStatus == 2) {
  658. throw BusinessRuntimeException.getInstance("大学生身份审核中");
  659. }
  660. }
  661. @Override
  662. public String generateSchema(String accessToken, TargetAppletSchemeReq targetAppletSchemeReq) throws Exception {
  663. String url = "https://api.weixin.qq.com/wxa/generatescheme?access_token=" + accessToken;
  664. HttpResponse<byte[]> res = J11HttpC.custom()
  665. .ofPost()
  666. .url(url)
  667. .body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(targetAppletSchemeReq)))
  668. .send(HttpResponse.BodyHandlers.ofByteArray());
  669. if (200 != res.statusCode()) {
  670. throw BusinessRuntimeException.getInstance("获取用户信息失败: " + IoKit.toString(res.body()));
  671. }
  672. JSONObject jsonObject = Jsons.parseObject(res.body(), JSONObject.class);
  673. if (jsonObject.getInt("errcode") != 0) {
  674. throw BusinessRuntimeException.getInstance(jsonObject.getStr("errmsg"));
  675. }
  676. if (StrUtil.isEmpty(jsonObject.getStr("openlink"))) {
  677. throw BusinessRuntimeException.getInstance("系统异常..");
  678. }
  679. return jsonObject.getStr("openlink");
  680. }
  681. @Override
  682. public byte[] generateCode(String accessToken, String path) throws Exception {
  683. String url = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + accessToken;
  684. JSONObject params = new JSONObject();
  685. params.putOpt("path", path);
  686. HttpResponse<byte[]> res = J11HttpC.custom()
  687. .ofPost()
  688. .url(url)
  689. .body(HttpRequest.BodyPublishers.ofString(params.toString()))
  690. .send(HttpResponse.BodyHandlers.ofByteArray());
  691. if (200 != res.statusCode()) {
  692. throw BusinessRuntimeException.getInstance("获取用户信息失败: " + IoKit.toString(res.body()));
  693. }
  694. return res.body();
  695. }
  696. }