@TedZhou
2020-10-15T06:30:40.000000Z
字数 10506
阅读 598
java
记录java开发微信网页认证、微信分享、微信支付等用到的方法。
public class WeixinSercice {final static String URL_SNS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code";final static String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";final static String URL_JSAPI_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi";final static String URL_ORDER_QUERY = "https://api.mch.weixin.qq.com/pay/orderquery";final static String URL_UNIFIED_ORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";public static final String TRADE_TYPE_H5 = "MWEB";public static final String TRADE_TYPE_JS = "JSAPI";final static String KEY_SIGN = "sign";@Value("${weixin.appid}")private String appId;@Value("${weixin.secret}")private String appSecret;@Value("${weixin.mch_id}")private String mchId;@Value("${weixin.mch_key}")private String mchKey;@Value("${weixin.notify_url}")private String notifyUrl;@AutowiredRestTemplate restTemplate;@AutowiredOrderService orderService;@Resourceprivate CacheManager cacheManager;private static final String CACHE_NAME_WEIXIN = "myapp:weixin";private static final String CACHE_KEY_ACCESS_TOKEN = "actoken";private static final String CACHE_KEY_JSAPI_TICKET = "jsticket";private Cache getCache() {return cacheManager.getCache(CACHE_NAME_WEIXIN);}// 微信网页认证:通过code获取tokenpublic WxWebToken fetchWebTokenByCode(String code) {String url = String.format(URL_SNS_TOKEN, appId, appSecret, code);String content = restTemplate.getForObject(url, String.class);WxWebToken token = JSON.parse(content, WxWebToken.class);return token;}// 微信JSSDK:获取指定url的configpublic WxJsdkConfig genJsdkConfig(String url) {WxJsdkConfig config = new WxJsdkConfig();config.setAppId(appId);config.setNonceStr(StringUtils.uuid());config.setTimestamp(DateTimeUtils.secondsOf(LocalDateTime.now()));String ticket = getJsapiTicket(url);if (StringUtils.isNotBlank(ticket)) {String string = "jsapi_ticket=" + ticket +"&noncestr=" + config.getNonceStr() +"×tamp=" + config.getTimestamp() +"&url=" + url;String signature = StringUtils.SHA1(string);if (StringUtils.isNotBlank(signature)) {config.setSignature(signature);}} else {config.setErrcode(1);config.setErrmsg("invalid ticket");}log.debug("genJsdkConfig for {} return {}", url, config.getSignature());return config;}//get cached jsapi_ticketprivate String getJsapiTicket(String url) {String cacheKey = CACHE_KEY_JSAPI_TICKET+StringUtils.MD5(url);WxJsapiTicket ticket = getCache().get(cacheKey, WxJsapiTicket.class);if (ticket == null || ticket.getExpired()) {ticket = fetchJsapiTicket();if (ticket != null && !ticket.getExpired()) {getCache().put(cacheKey, ticket);}else {return null;}} else {log.debug("return cached {} for key {}", CACHE_NAME_WEIXIN, cacheKey);}return ticket.getTicket();}private WxJsapiTicket fetchJsapiTicket() {String accessToken = getAccessToken();if (StringUtils.isBlank(accessToken)) {return null;}String url = String.format(URL_JSAPI_TICKET, accessToken);String content = restTemplate.getForObject(url, String.class);WxJsapiTicket ticket = JSON.parse(content, WxJsapiTicket.class);if (ticket != null) {Long expires = ticket.getExpires_in();if (expires != null) {//把过期秒数转化为世纪秒expires += DateTimeUtils.secondsOf(LocalDateTime.now());}else{expires = 0L;}ticket.setExpires_in(expires);log.debug("fetchJsapiTicket return {}-{}", ticket.getErrcode(), ticket.getErrmsg());}return ticket;}// get cached access_tokenprivate String getAccessToken() {WxAccessToken token = getCache().get(CACHE_KEY_ACCESS_TOKEN, WxAccessToken.class);if (token == null || token.getExpired()) {token = fetchAccessToken();if (token != null && !token.getExpired()) {getCache().put(CACHE_KEY_ACCESS_TOKEN, token);}} else {log.debug("return cached {} for key {}", CACHE_NAME_WEIXIN, CACHE_KEY_ACCESS_TOKEN);}return token.getAccess_token();}private WxAccessToken fetchAccessToken() {String url = String.format(URL_ACCESS_TOKEN, appId, appSecret);String content = restTemplate.getForObject(url, String.class);WxAccessToken token = JSON.parse(content, WxAccessToken.class);if (token != null) {Long expires = token.getExpires_in();if (expires != null) {//把过期秒数转化为世纪秒expires += DateTimeUtils.secondsOf(LocalDateTime.now());}else{expires = 0L;}token.setExpires_in(expires);if (StringUtils.isNotBlank(token.getErrmsg())){log.debug("fetchAccessToken return {}-{}", token.getErrcode(), token.getErrmsg());}else{log.debug("fetchAccessToken return {}", token.getAccess_token());}} else {log.debug("fetchAccessToken return null");}return token;}//处理订单public Order processOrder(Order order) {if (StringUtils.isNotBlank(order.getId())) {Order dbOrder = orderService.findById(order.getId());if (dbOrder != null && dbOrder.getStatus()>=Order.STATUS_PAYED) {return dbOrder;//已支付}}order = orderService.upsert(order);Map<String, String> map = placeOrder(order);if (map != null) {String returnCode = map.get("return_code");order.setReturnCode(returnCode);if ("SUCCESS".equals(returnCode)) {order.setStatus(Order.STATUS_ORDER);}order.setReturnMsg(map.get("return_msg"));order.setMwebUrl(map.get("mweb_url"));order.setPrepayId(map.get("prepay_id"));orderService.save(order);orderService.sendNotify(order);if (TRADE_TYPE_JS.equals(order.getTradeType())) {Map<String, String> signs = new TreeMap<>();signs.put("appId", appId);signs.put("nonceStr", StringUtils.uuid());signs.put("package", "prepay_id="+map.get("prepay_id"));signs.put("signType", "MD5");signs.put("timeStamp", String.valueOf(DateTimeUtils.secondsOf(LocalDateTime.now())));signs.put("paySign", genSign(signs));order.setSigns(signs);}}return order;}//下单private Map<String, String> placeOrder(Order order) {Map<String, String> map = null;String tradeType = order.getTradeType();if (TRADE_TYPE_H5.equals(tradeType)) {map = prepareH5Order(order.getTradeNo(), order.getProductId(), order.getProductName(), order.getTotalFee(), order.getIpaddr());}else{map = prepareJsOrder(order.getTradeNo(), order.getProductId(), order.getProductName(), order.getTotalFee(), order.getIpaddr(), order.getOpenid());}HttpEntity<String> request = genXmlRequest(map);String res = restTemplate.postForObject(URL_UNIFIED_ORDER, request, String.class);log.debug(res);return XmlUtils.parse(res);}// H5支付下单数据private Map<String, String> prepareH5Order(String tradeNo, String productId, String productName, Long amount, String ip) {Map<String, String> order = newOrderMap();order.put("trade_type", TRADE_TYPE_H5);//H5支付的交易类型为MWEBorder.put("notify_url", notifyUrl);//回调地址, 不能携带参数。order.put("scene_info", "{\"h5_info\": {\"type\":\"WAP\",\"wap_url\": \"\",\"wap_name\": \"\"}}");//用于上报支付的场景信息order.put("spbill_create_ip", ip);//用户端IP,支持ipv4、ipv6格式order.put("out_trade_no", tradeNo);//自定义交易单号order.put("product_id", productId);//自定义商品order.put("body", productName);//网页的主页title名-商品概述order.put("fee_type", "CNY");//境内只支持CNY,默认可不传order.put("total_fee", String.valueOf(amount));//订单总金额,单位为分//签名order.put(KEY_SIGN, genSign(order));return order;}// JSAPI支付下单数据private Map<String, String> prepareJsOrder(String tradeNo, String productId, String productName, Long amount, String ip, String openid) {Map<String, String> order = newOrderMap();order.put("trade_type", TRADE_TYPE_JS);//交易类型为JSAPIorder.put("notify_url", notifyUrl);//回调地址, 不能携带参数。//order.put("scene_info", "{\"h5_info\": {\"type\":\"WAP\",\"wap_url\": \"\",\"wap_name\": \"\"}}");//用于上报支付的场景信息order.put("openid", openid);order.put("spbill_create_ip", ip);//用户端IP,支持ipv4、ipv6格式order.put("out_trade_no", tradeNo);//自定义交易单号order.put("product_id", productId);//自定义商品order.put("body", productName);//网页的主页title名-商品概述order.put("fee_type", "CNY");//境内只支持CNY,默认可不传order.put("total_fee", String.valueOf(amount));//订单总金额,单位为分//签名order.put(KEY_SIGN, genSign(order));return order;}// 下单数据准备:公用部分private Map<String, String> newOrderMap() {Map<String, String> order = new TreeMap<>();order.put("appid", appId);order.put("mch_id", mchId);order.put("nonce_str", StringUtils.uuid());return order;}// 构造xml requestprivate HttpEntity<String> genXmlRequest(Object data) {HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_XML);String body = XmlUtils.stringify(data, "xml");return new HttpEntity<String>(body, headers);}// 生成签名private String genSign(Map<String, String> paramMap) {StringBuilder sb = new StringBuilder();paramMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach((a) -> {if (StringUtils.isBlank(a.getKey()) || KEY_SIGN.equals(a.getKey())) {return;}if (StringUtils.isBlank(a.getValue())) {return;}sb.append(a.getKey()); sb.append("="); sb.append(a.getValue()); sb.append("&");});sb.append("key="); sb.append(mchKey);String signStr = sb.toString();log.debug(signStr);return StringUtils.MD5(signStr).toUpperCase();}//微信支付结果回调处理@Synchronized // TODO: 避免重入仅这样不够,还需要锁定订单记录public String processCallback(String data) {Map<String, String> map = XmlUtils.parse(data);String sign = genSign(map);if (!sign.equals(map.get(KEY_SIGN))) {return returnCodeMsg("FAIL", "SIGNERROR");};String tradeNo = map.get("out_trade_no");Order order = orderService.findByTradeNo(tradeNo);if (order == null) {return returnCodeMsg("FAIL", "NOTFOUND");}if (order.getStatus() >= Order.STATUS_PAYED) {return returnCodeMsg("SUCCESS", "OK!");}String tradeType = map.get("trade_type");long totalFee = NumberUtils.parse(map.get("total_fee"), Long.class, 0L);if (totalFee != order.getTotalFee() || !StringUtils.equals(tradeType, order.getTradeType())) {return returnCodeMsg("FAIL", "TRADEINFOERROR");}order.setTransactionId(map.get("transaction_id"));order.setReturnCode(map.get("return_code"));order.setResultCode(map.get("result_code"));order.setBankType(map.get("bank_type"));order.setTimeEnd(map.get("time_end"));order.setStatus(Order.STATUS_PAYED);orderService.save(order);return returnCodeMsg("SUCCESS", "OK");}private String returnCodeMsg(String code, String msg) {return String.format("<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>", code, msg);}}## WxResponse.java```java@Datapublic class WxResponse implements Serializable {private Integer errcode;private String errmsg;}<div class="md-section-divider"></div>
@Datapublic class WxResexpire extends WxResponse implements Serializable {Long expires_in;public Boolean getExpired() {return expires_in == null || expires_in <= DateTimeUtils.secondsOf(LocalDateTime.now());}}<div class="md-section-divider"></div>
@Datapublic class WxAccessToken extends WxResexpire implements Serializable {String access_token;}<div class="md-section-divider"></div>
@Datapublic class WxWebToken extends WxAccessToken implements Serializable {String refresh_token;String openid;String scope;}<div class="md-section-divider"></div>
@Datapublic class WxJsapiTicket extends WxResexpire implements Serializable {String ticket;}<div class="md-section-divider"></div>
@Data@JsonInclude(value = Include.NON_NULL)public class WxJsdkConfig extends WxResponse implements Serializable {String appId;Long timestamp;String nonceStr;String signature;}