package com.ydtech.modules.admin.components; import com.alibaba.fastjson.JSONObject; import com.ydtech.exception.SystemException; import com.ydtech.modules.admin.components.request.UnionPayRequest; import com.ydtech.modules.admin.components.response.UnionPayResponse; import com.ydtech.utils.HMAC256Uitil; import com.ydtech.utils.SM2Utils; import com.ydtech.utils.StringUtils; import lombok.RequiredArgsConstructor; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.web.client.RestTemplate; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.InvalidParameterException; import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.TimeUnit; /** * @description: 银联Api **/ @Component @RequiredArgsConstructor public class UnionPayRequestApiComponent { private final static Logger LOGGER = LoggerFactory.getLogger(UnionPayRequestApiComponent.class); @Autowired Environment environment; @Autowired private RedissonClient redissonClient; @Autowired private StringRedisTemplate redisTemplate; private final RestTemplate restTemplate; private final String UNION_PAY_MAP = "unionPay"; private final String UNION_PAY_ACCESS_TOKEN_KEY = "unionPayAccessToken"; private final String UNION_PAY_ACCESS_TOKEN_LOCK = "unionPayAccessTokenLock"; private final int UNION_PAY_LOCK_TIME = 10000; private final int UNION_PAY_SLEEP_TIME = 5000; private final int RANDOM_MAX = 999999999; private final int RANDOM_MIN = 100000000; /** * 运营商二要素验证接口 * @param request */ public UnionPayResponse twoFactorVerifyRequest(UnionPayRequest request) { String url = environment.getProperty("unionPay.2FactorVerifyUrl"); JSONObject params=new JSONObject(); if (StringUtils.isBlank(request.getPhoneNo())) { throw new InvalidParameterException("手机号不允许为空!"); } params.put("phoneNo", request.getPhoneNo()); //01:验证姓名+手机号,此时用户姓名必输 02:身份证+手机号,此时证件类型、证件 if (StringUtils.isNotBlank(request.getCertNo())) { params.put("verifyType", "02"); params.put("certType", "01");//证件类型 01:身份证 params.put("certNo", request.getCertNo()); } else if (StringUtils.isNotBlank(request.getName())) { params.put("verifyType", "01"); params.put("name", request.getName()); } else { throw new InvalidParameterException("身份证、用户姓名至少传值一个!"); } return commonRequest(url, params); } /** * 银行卡验证接口 * @param request */ public UnionPayResponse bankCardVerifyRequest(UnionPayRequest request) { String url = environment.getProperty("unionPay.2FactorVerifyUrl"); JSONObject params=new JSONObject(); if (StringUtils.isBlank(request.getCardNo())) { throw new InvalidParameterException("卡号不允许为空!"); } if (StringUtils.isBlank(request.getCertNo()) && StringUtils.isBlank(request.getName()) && StringUtils.isBlank(request.getPhoneNo())) { throw new InvalidParameterException("证件号、用户姓名、手机号至少传值一个!"); } params.put("cardNo", request.getCardNo()); params.put("personalMandate", "1");//是否取得个人授权,字段取值0或1。1:是 0:否 params.put("sceneId", "13"); //TODO 业务场景 13:车险 if (StringUtils.isNotBlank(request.getCertNo())) { params.put("certType", "01");//证件类型 01:身份证 params.put("certNo", request.getCertNo()); } if (StringUtils.isNotBlank(request.getName())) { params.put("name", request.getName()); } if (StringUtils.isNotBlank(request.getPhoneNo())) { params.put("phoneNo", request.getPhoneNo()); } params.put("appName", "01晋掌柜");//TODO 交易发起应用名称 params.put("ipType", "04");//IP版本号 04:IPV4 06:IPV6 try { params.put("sourceIp", InetAddress.getLocalHost().getHostAddress()); // ip } catch (UnknownHostException e) { LOGGER.error("获取ipv4地址异常",e); throw new RuntimeException("获取ipv4地址异常:",e); } params.put("protocolVersion", ""); // 用户授权协议版本号 //TODO params.put("protocolNo", ""); // 用户授权协议流水号 return commonRequest(url, params); } /** * 调用银联接口 * @param url 地址 * @param params 入参 * @return 返参 */ private UnionPayResponse commonRequest(String url,JSONObject params) { try { String publicKey = environment.getProperty("unionPay.publicKey"); JSONObject data=new JSONObject(); String dataValue = Base64.getEncoder().encodeToString(SM2Utils.encrypt(params.toString(), publicKey).getBytes()); data.put("data", dataValue); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Content-Type","application/json;charset=UTF-8"); httpHeaders.set("Accept","application/json"); httpHeaders.set("Authorization",getAccessToken()); // 请求 HttpEntity httpEntity = new HttpEntity<>(params.toString(), httpHeaders); ResponseEntity response = restTemplate.postForEntity(url, httpEntity, String.class); JSONObject resp = JSONObject.parseObject(response.getBody()); if ("20000000".equals(resp.getString("errCode"))) { String privateKey = environment.getProperty("unionPay.privateKey"); String respData = SM2Utils.decrypt(new String(Base64.getDecoder().decode(resp.getString("data"))), privateKey); return JSONObject.parseObject(respData, UnionPayResponse.class); } else { throw new SystemException("银联信息验证异常["+resp.getString("errCode")+":"+ resp.getString("errInfo")+"]"); } } catch (Exception e) { LOGGER.error("银联接口调用异常",e); throw new RuntimeException("银联接口调用异常:",e); } } /** * 获取银联token * @return */ private String getAccessToken() { Object token = redisTemplate.opsForHash().get(UNION_PAY_MAP,UNION_PAY_ACCESS_TOKEN_KEY); // 缓存中不存在就去请求token if (!ObjectUtils.isEmpty(token)) { return (String)token; } // 使用分布式锁 只有一个人可以访问登录 RLock lock = redissonClient.getLock(UNION_PAY_ACCESS_TOKEN_LOCK); try { boolean isLock = lock.tryLock(1, UNION_PAY_LOCK_TIME, TimeUnit.MILLISECONDS); if (isLock) { try { return this.invokeToken(); } finally { lock.unlock(); } } } catch (InterruptedException e) { LOGGER.error("getAccessToken InterruptedException",e); lock.unlock(); Thread.currentThread().interrupt(); throw new RuntimeException("获取银联Token异常,请重试!"); } // 睡眠一段时间 自调用 重新获取缓存中的 header try { Thread.sleep(UNION_PAY_SLEEP_TIME); } catch (InterruptedException e) { LOGGER.error("getAccessToken InterruptedException",e); Thread.currentThread().interrupt(); throw new RuntimeException("获取银联Token异常,请重试!"); } return getAccessToken(); } /** * 调用银联接口获取token * @return token */ private String invokeToken() { String appId = environment.getProperty("unionPay.appId"); String appKey = environment.getProperty("unionPay.appKey"); String accessTokenUrl = environment.getProperty("unionPay.accessTokenUrl"); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); String timestamp = sdf.format(new Date()); Random rand = new Random(); String randNumber = String.valueOf(rand.nextInt(RANDOM_MAX - RANDOM_MIN + 1) + RANDOM_MIN); String signature = HMAC256Uitil.hmac256(appId+timestamp+randNumber,appKey);//TODO JSONObject params=new JSONObject(); params.put("appId", appId); params.put("timestamp", timestamp); params.put("nonce", randNumber); params.put("signMethod", "SHA256"); params.put("signature", signature); try{ HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("Content-Type","application/json;charset=UTF-8"); httpHeaders.set("Accept","application/json"); // 请求 HttpEntity httpEntity = new HttpEntity<>(params.toString(), httpHeaders); ResponseEntity response = restTemplate.postForEntity(accessTokenUrl, httpEntity, String.class); JSONObject resp = JSONObject.parseObject(response.getBody()); if ("20000000".equals(resp.getString("errCode"))) { String token = resp.getString("accessToken"); int expiresIn = resp.getIntValue("expiresIn"); //失效时间 Map map = new HashMap<>(); map.put(UNION_PAY_MAP, "OPEN-ACCESS-TOKEN AccessToken=\""+token+"\", appId=\""+appId+"\""); redisTemplate.opsForHash().putAll(UNION_PAY_ACCESS_TOKEN_KEY, map); redisTemplate.expire(UNION_PAY_ACCESS_TOKEN_KEY, expiresIn-30, TimeUnit.SECONDS); return token; } else { throw new SystemException("获取银联Token异常["+resp.getString("errCode")+":"+ resp.getString("errInfo")+"]"); } }catch(Exception e){ LOGGER.error("获取银联Token异常",e); throw new RuntimeException("获取银联Token异常,请重试!",e); } } }