java微信开发常用方法 - zhouted/zhouted.github.io GitHub Wiki

标签: java wechat 微信网页认证 微信分享 微信支付

WeixinService.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;
	
	@Autowired
	RestTemplate restTemplate;
    @Autowired
	OrderService orderService;
	@Resource
	private 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获取token
	public 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的config
	public 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() +
					"&timestamp=" + 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_ticket
	private 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_token
	private 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支付的交易类型为MWEB
		order.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);//交易类型为JSAPI
		order.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 request
	private 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
@Data
public class WxResponse implements Serializable {
	private Integer errcode;
	private String errmsg;
}

WxResexpire.java

@Data
public class WxResexpire extends WxResponse implements Serializable {
	Long expires_in;
	public Boolean getExpired() {
		return expires_in == null || expires_in <= DateTimeUtils.secondsOf(LocalDateTime.now());
	}
}

WxAccessToken.java

@Data
public class WxAccessToken extends WxResexpire implements Serializable {
	String access_token;
}

WxWebToken.java

@Data
public class WxWebToken extends WxAccessToken implements Serializable {
	String refresh_token;
	String openid;
	String scope;
}

WxJsapiTicket.java

@Data
public class WxJsapiTicket extends WxResexpire implements Serializable {
	String ticket;
}

WxJsdkConfig.java

@Data
@JsonInclude(value = Include.NON_NULL)
public class WxJsdkConfig extends WxResponse implements Serializable {
	String appId;
	Long timestamp;
	String nonceStr;
	String signature;
}
⚠️ **GitHub.com Fallback** ⚠️