微信JSSDK之添加微信卡券

微信卡券的使用,是在之前的微信jsapi基础上,再加上一次卡券的单独验签,

这里对上篇微信JSSDK的使用稍作修改:

1.微信的accessToken的获取有时间限制,之前是将token的读取放在一个单独的服务上,单控

2.基于开个别的服务比较繁琐,现在使用redis缓存,来控制访问频率,至于并发,由锁来控制

代码如下:

package com.mazing.commons.wx;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.type.TypeReference;
import com.mazing.commons.cache.redis.Redis;
import com.mazing.commons.cache.redis.RedisLock;
import com.mazing.commons.server.context.MainframeBeanFactory;
import com.mazing.commons.utils.HttpClientUtils;
import com.mazing.commons.utils.JsonUtils;
import com.mazing.commons.utils.LogUtil;
import com.mazing.commons.utils.cfg.EncryptionPropertyPlaceholderConfigurer;

/**
 * access token 改为缓存读取方式,失效再读取
 * 
 * @author sky
 *
 */
public class KFwxAccessTokenReader {

	private static final Logger logger = LoggerFactory.getLogger(KFwxAccessTokenReader.class);

	/**
	 * 获取间隔
	 */
	private static final int INTERVAL = 2 * 60 ;

	// access token 在 7200秒过期,
	private static int TOKEN_TIMEOUT = 7200 - INTERVAL;

	private static String appid = null;

	private static String secret = null;

	/**
	 * access token
	 */
	public String wxAccessToken = "";

	/**
	 * jsapi_ticket
	 */
	public String wxJsapiTicket = "";

	/**
	 * 卡券api_ticket
	 */
	public String wxCardApiTicket = "";

	private static Redis redis;

	static {

		readConfig();
		//clearCache();
	}

	public static Map<String, String> readAccessToken() {

		Map<String, String> map = new HashMap<>();

		String atKey = getJsAccessTokenCacheKey();
		String accessToken = redis().get(atKey);
		String jsapiTicket = "";
		String cardApiTicket = "";
		if (StringUtils.isBlank(accessToken)) {

			accessToken = readToken();
			jsapiTicket = readTicket(accessToken);
			cardApiTicket = readCardTicket(accessToken);

			logger.info("kf#wx#accessToken | cache为空,通过接口重新获取数据 | accessToken: {}, jsapiTicket: {},cardApiTicket: {}", LogUtil.hidePassport(accessToken),
					 LogUtil.hidePassport(jsapiTicket),  LogUtil.hidePassport(cardApiTicket));
		} else {

			jsapiTicket = redis().get(getJsapiTicketCacheKey());
			cardApiTicket = redis().get(getCardApiTicketCacheKey());

			logger.info("kf#wx#accessToken | cache不为空, 获取缓存数据 | accessToken: {}, jsapiTicket: {},cardApiTicket: {}", LogUtil.hidePassport(accessToken),
					 LogUtil.hidePassport(jsapiTicket),  LogUtil.hidePassport(cardApiTicket));
		}

		map.put(WeixinAccessToken.KF_ACCESS_TOKEN, accessToken);
		map.put(WeixinAccessToken.KF_JSAPI_TICKET, jsapiTicket);
		map.put(WeixinAccessToken.KF_CARD_API_TICKET, cardApiTicket);

		return map;

	}

	private static String getJsAccessTokenCacheKey() {
		return "KF_wxAccessTokenKey";
	}

	/**
	 * jsapi_ticket cache key
	 * 
	 * @return
	 */
	private static String getJsapiTicketCacheKey() {
		return "KF_wxJSapiTicketKey";
	}

	/**
	 * 卡券 api_ticket cache key
	 * 
	 * @return
	 */
	private static String getCardApiTicketCacheKey() {
		return "KF_wxCardApiTicketKey";
	}

	public static void clearCache() {

		redis().del(getJsAccessTokenCacheKey());
		redis().del(getJsapiTicketCacheKey());
		redis().del(getCardApiTicketCacheKey());

		logger.info("清除微信卡券相关缓存....");
	}

	private static Redis redis() {
		if (null == redis)
			redis = (Redis) MainframeBeanFactory.getBean("redis");
		return redis;
	}

	/**
	 * 读取配置,只读一次
	 * 
	 * @throws Exception
	 */
	public static void readConfig() {

		appid = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_appid");
		secret = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_appsecret");
	}

	/**
	 * 读取 access token
	 */
	private static String readToken() {

		RedisLock lock = new RedisLock(redis, "KFWxAccessTokenKey");

		String wxAccessToken = "";

		if (lock.lock()) {
			Map<String, String> params = new HashMap<>(4);
			params.put("grant_type", "client_credential");
			params.put("appid", appid);
			params.put("secret", secret);
			String json = HttpClientUtils.doGet("https://api.weixin.qq.com/cgi-bin/token", 10000, params);
			if (StringUtils.isBlank(json)) {
				logger.error("读取微信 access token 错误");
			}
			Map<String, Object> map = JsonUtils.parseObject(json.trim(), new TypeReference<Map<String, Object>>() {
			});
			if (map.containsKey("access_token")) {
				wxAccessToken = (String) map.get("access_token");
				logger.info("读取微信 access token 成功");
				if (map.containsKey("expires_in")) {
					int expiresIn = ((Number) map.get("expires_in")).intValue();
					logger.info("过期时间 (s):" + expiresIn);
					TOKEN_TIMEOUT = expiresIn  - INTERVAL;
					// 缓存,并设置过期时间
					redis().set(getJsAccessTokenCacheKey(), wxAccessToken, TOKEN_TIMEOUT);
				}

			} else {
				logger.error("读取微信 access token 错误:" + json);
			}

			lock.unlock();
		}

		return wxAccessToken;
	}

	/**
	 * 读取 jsapi_ticket
	 */
	private static String readTicket(String wxAccessToken) {

		String wxJsapiTicket = "";

		Map<String, String> params = new HashMap<>(4);
		params.put("access_token", wxAccessToken);
		params.put("type", "jsapi");
		String json = HttpClientUtils.doGet("https://api.weixin.qq.com/cgi-bin/ticket/getticket", 10000, params);
		Map<String, Object> map = JsonUtils.parseObject(json.trim(), new TypeReference<Map<String, Object>>() {
		});
		int errcode = -1;
		if (map.get("errcode") != null) {
			errcode = ((Number) map.get("errcode")).intValue();
			logger.info("读取微信 jsapi_ticket 结果,errcode: {}, errmsg: {}", errcode, map.get("errmsg"));
			if (errcode == 0) {
				wxJsapiTicket = (String) map.get("ticket");

				// set to cache
				redis().set(getJsapiTicketCacheKey(), wxJsapiTicket);

				logger.info("读取微信 jsapi_ticket 成功!");
			}
		} else {
			logger.error("读取微信 jsapi_ticket 错误:" + json);
		}

		return wxJsapiTicket;
	}

	/**
	 * 获取卡券 api_ticket
	 */
	private static String readCardTicket(String wxAccessToken) {

		String wxCardApiTicket = "";

		Map<String, String> params = new HashMap<>(4);
		params.put("access_token", wxAccessToken);
		params.put("type", "wx_card");
		String json = HttpClientUtils.doGet("https://api.weixin.qq.com/cgi-bin/ticket/getticket", 10000, params);
		Map<String, Object> map = JsonUtils.parseObject(json.trim(), new TypeReference<Map<String, Object>>() {
		});
		int errcode = -1;
		if (map.get("errcode") != null) {
			errcode = ((Number) map.get("errcode")).intValue();
			logger.info("读取微信  卡券 api_ticket 结果,errcode: {}, errmsg: {}", errcode, map.get("errmsg"));
			if (errcode == 0) {

				wxCardApiTicket = (String) map.get("ticket");
				// set to cache
				redis().set(getCardApiTicketCacheKey(), wxCardApiTicket);

				logger.info("读取微信 卡券 api_ticket 成功! ticket: {}", LogUtil.hidePassport(wxCardApiTicket));
			}
		} else {
			logger.error("读取微信 卡券 api_ticket 错误:" + json);
		}

		return wxCardApiTicket;
	}

}

util类

package com.mazing.mobile.web.kaifun;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mazing.CommonConstants;
import com.mazing.commons.utils.JsonUtils;
import com.mazing.commons.utils.LogUtil;
import com.mazing.commons.utils.cfg.EncryptionPropertyPlaceholderConfigurer;
import com.mazing.commons.utils.encrypt.EncryptUtil;
import com.mazing.commons.wx.KFwxAccessTokenReader;
import com.mazing.commons.wx.WeixinAccessToken;
import com.mazing.core.web.ResultCode;
import com.mazing.core.web.exception.ServiceException;

/**
 * 对接公众号的微信平台 工具类(验签参数获取)
 * 
 * @author sky 2016-05-57
 *
 */
public class KaiFunWXUtil {

	private static final Logger logger = LoggerFactory.getLogger(KaiFunWXUtil.class);

	/**
	 * 微信分享的attr
	 */
	public static final String WX_CONFIG_ATTR = "wxConfig";

	/**
	 * 开饭 微信appid
	 */
	private static String KF_WX_APPID = null;

	// 原始值
	public static String KF_WX_CARDID = null;

	/**
	 * 检查当前的请求是否是 https 的,如果不是则重定向成为https<br>
	 * 如果执行了重定向,返回true
	 */
	public static boolean redirectFullUrl2Https(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String params = request.getQueryString();
		String url = request.getRequestURL().toString();
		String scheme = request.getHeader("x-forwarded-proto");

		boolean need2https = (url.startsWith("http://") // 如果是http的请求
		&& (StringUtils.isBlank(scheme) || !("https".equals(scheme))));// nginx上报的不是https协议

		// 重定向请求为https,并返回true
		if (need2https) {
			url = url.replaceFirst("http://", "https://");

			if (params != null)
				url += "?" + params;

			response.sendRedirect(url);
			return true;
		}

		return false;
	}

	/**
	 * 返回当前页面完整的 URL,包括“?”后面的参数
	 * 
	 * @param request
	 * @return
	 */
	public static String getFullUrl(HttpServletRequest request) {
		String params = request.getQueryString();
		String url = request.getRequestURL().toString();

		// nginx 的 upstream配置使用的是 http,导致外部是https的请求,来到代码却是http
		// nginx 会带上scheme参数(443端口才设置)
		String scheme = request.getHeader("x-forwarded-proto");
		if (StringUtils.isNotBlank(scheme) && "https".equals(scheme))
			url = url.replaceFirst("http://", "https://");

		if (params != null) {
			url += "?" + params;
		}
		return url;
	}

	public static void main(String[] args) {
		String url = "http://xxx.com?callback=http://123.com";
		System.out.println(url.replaceFirst("http", "https"));
	}

	/**
	 * 微信公众号的appid
	 * 
	 * @return
	 */
	private static String getWxMpAppId() {
		if (KF_WX_APPID == null) {
			KF_WX_APPID = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_appid");
		}
		return KF_WX_APPID;
	}

	private static String getWxMpCardId() {
		if (KF_WX_CARDID == null) {
			KF_WX_CARDID = (String) EncryptionPropertyPlaceholderConfigurer.getConfig().get("kf_cardId");
		}
		return KF_WX_CARDID;
	}

	/**
	 * 微信分享的 config
	 * 
	 * @param url 当前页面完整的地址,包括参数
	 * @return
	 */
	public static String getWxMpConfig(String url) {
		String noncestr = RandomStringUtils.random(16, CommonConstants.LETTER_NUMBER_CHARS);
		long timestamp = System.currentTimeMillis() / 1000; // 秒

		String appId = getWxMpAppId();

		Map<String, String> amap = KFwxAccessTokenReader.readAccessToken();
		String jsapiTicket = amap.get(WeixinAccessToken.KF_JSAPI_TICKET);

		logger.info("mobile#share#getWxMpConfig | 设置微信分享配置 |url: {}, KF_JSAPI_TICKET: {}", url, LogUtil.hidePassport(jsapiTicket));
		String sourceStr = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + noncestr + "&timestamp=" + timestamp + "&url=" + url;

		String signature = EncryptUtil.getSHA1(sourceStr).toLowerCase();
		String[] jsApiList = new String[] { //
		"checkJsApi", //
				"onMenuShareTimeline", //
				"onMenuShareAppMessage",//
				"onMenuShareQQ",//
				"onMenuShareWeibo", //
				"onMenuShareQZone",//
				"hideAllNonBaseMenuItem",//
				"addCard",//
				"showAllNonBaseMenuItem" };
		Map<String, Object> map = new HashMap<>(8);
		map.put("nonceStr", noncestr);
		map.put("timestamp", timestamp);
		map.put("appId", appId);
		map.put("signature", signature);
		map.put("debug", false);
		map.put("jsApiList", jsApiList);
		return JsonUtils.toJSONString(map);
	}

	/**
	 * 获取卡券的签名
	 * 
	 * @return
	 */
	public static void setCardSignature(HttpServletRequest request) {

		long timestamp = System.currentTimeMillis() / 1000; // 秒

		String cardId = getWxMpCardId();
		String timeStr = timestamp + "";

		Map<String, String> map = KFwxAccessTokenReader.readAccessToken();
		String apiTicket = map.get(WeixinAccessToken.KF_CARD_API_TICKET);

		if (StringUtils.isBlank(apiTicket)) {
			throw new ServiceException(ResultCode.FAILURE, "系统繁忙, 请稍后再试");
		}

		logger.info("kaifun#wx#signature | 验签参数 | cardId: {}, timestamp: {}, apiTicket: {}", cardId, timeStr, LogUtil.hidePassport(apiTicket));

		List<String> list = new ArrayList<>();

		list.add(timeStr);
		list.add(cardId);
		list.add(apiTicket);

		Collections.sort(list);

		String sourceStr = "";

		for (Object obj : list) {
			sourceStr += obj;
		}

		String signature = EncryptUtil.getSHA1(sourceStr).toLowerCase();

		//logger.info("kaifun#wx#signature | 验签参数排序后 | sourceStr: {}, signature:  {}", sourceStr, signature);

		Map<String, Object> ext = new HashMap<>();

		ext.put("timestamp", timeStr);
		ext.put("signature", signature);

		request.setAttribute("ext", JsonUtils.toJSONString(ext));

		request.setAttribute("_cardid", KF_WX_CARDID);

	}

	/**
	 * 设置微信分享JSSDK 需要的config配置信息<br>
	 * 
	 * 凡是需要设置分享操作菜单(分享至朋友圈、QQ、微博, 或者屏蔽分享功能,只保留设置字体、刷新 等 菜单) 相关的页面, 都需要调用该方法进行config设置
	 * 
	 * @param model
	 * @param request
	 * @author sky 2016-04-19
	 */
	public static void setWxJsConfig(HttpServletRequest request) {
		// 微信相关的内容
		String url = getFullUrl(request);

		request.setAttribute(WX_CONFIG_ATTR, getWxMpConfig(url));

		setCardSignature(request);

	}

}

controller下发

package com.mazing.mobile.web.kaifun;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/open/wx/card")
public class KFCardController {

	@RequestMapping("obtain/index")
	public String searchIndex(Model model, HttpServletRequest request) {


		// 微信相关的内容
		KaiFunWXUtil.setWxJsConfig(request);
		
		return "/open/kaifun/obtain_index";
	}
}

jsp页面

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<script type="text/javascript"
	src="https://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script type="text/javascript">

	
	// 微信分享的配置
	wx.config(<c:out value="${wxConfig}" escapeXml="false"/>);

	wx.error(function(res) {
		//alert(JSON.stringify(res));
	});

	/**
	 http://www.xiaomeiti.com/note/3561
	 **/
	function WeixinShare(shareData) {
		this.shareData = shareData;
		if (wx && wx.checkJsApi) {
			this.shareType = "api";
			this.initByAPI();
		} else {
			this.shareType = "bridge";
			this.initByBridge();
		}
	}

	WeixinShare.prototype.initByAPI = function() {
		var me = this;
		

		wx.ready(function() {
			
			 document.querySelector('#addCard').onclick = function () {
                 wx.addCard({
                   cardList: [
                     {
                       cardId: '${_cardid}',
                       cardExt: '<c:out value="${ext}" escapeXml="false"/>'
                     }
                   ],
                   success: function (res) {
                     alert('已添加卡券:' + JSON.stringify(res.cardList));
                   }
                 });
             };
             
             /* */   

			var shareData = {
				title : me.getParam("title"),
				desc : me.getParam("desc"),
				link : me.getParam("link"),
				imgUrl : me.getParam("imgUrl"),
				trigger : function(res) {
					/*
					this.title = me.getParam("title");
					this.desc = me.getParam("desc");
					this.link = me.getParam("link");
					this.imgUrl = me.getParam("imgUrl");
					 */
				}
			};
			
			wx.onMenuShareAppMessage(shareData);

			var shareData2 = {
				title : me.getParam("title"),
				desc : me.getParam("desc"),
				link : me.getParam("link"),
				imgUrl : me.getParam("imgUrl"),
				trigger : function(res) {
				}
			};
			var timelineTitle = me.getParam("timelineTitle");
			if (timelineTitle) {
				shareData2.title = timelineTitle;
			}
			wx.onMenuShareTimeline(shareData2);//分享朋友圈
			wx.onMenuShareQQ(shareData);//分享至QQ
			wx.onMenuShareWeibo(shareData);//分享到微博
			wx.onMenuShareQZone(shareData);//分享到QZone
			
			
			//这里调用show all,是因为其他页面可能会调用了hide all(貌似是全局生效) 后跳转至 需要show all的页面
			//wx.showAllNonBaseMenuItem({
			//	success : function() {
			//		//MazingEnv.log('已显示所有非基本菜单项');
			//	}
			//});
		});
	};

	WeixinShare.prototype.initByBridge = function() {
		var me = this;
		document.addEventListener('WeixinJSBridgeReady',
				function onBridgeReady() {
					window.alert('Bridge triggered.');
					WeixinJSBridge.on('menu:share:appmessage', function(argv) {
						me.shareFriend()
					});
					WeixinJSBridge.on('menu:share:timeline', function(argv) {
						me.shareTimeline()
					});
				}, false);
	};

	WeixinShare.prototype.getParam = function(name) {
		var val = this.shareData[name];
		if (typeof val == "function") {
			return val();
		}

		return val;
	};

	WeixinShare.prototype.shareFriend = function() {
		WeixinJSBridge.invoke('sendAppMessage', {
			appid : this.getParam("appid"),
			img_url : this.getParam("imgUrl"),
			img_width : 120,
			img_height : 120,
			link : this.getParam("link"),
			title : this.getParam("title"),
			desc : this.getParam("desc")
		}, function(res) {
			_report('send_msg', res.err_msg);
		});
	};

	WeixinShare.prototype.shareTimeline = function() {
		WeixinJSBridge.invoke('shareTimeline', {
			appid : this.getParam("appid"),
			img_url : this.getParam("imgUrl"),
			img_width : 120,
			img_height : 120,
			link : this.getParam("link"),
			title : this.getParam("title"),
			desc : this.getParam("desc")
		}, function(res) {
			_report('timeline', res.err_msg);
		});
	};

	var wxApi = new WeixinShare(shareData);
</script>

相关推荐