@TedZhou
2020-10-15T14:30:40.000000Z
字数 10506
阅读 353
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;
@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() +
"×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_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;
}
<div class="md-section-divider"></div>
@Data
public 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>
@Data
public class WxAccessToken extends WxResexpire implements Serializable {
String access_token;
}
<div class="md-section-divider"></div>
@Data
public class WxWebToken extends WxAccessToken implements Serializable {
String refresh_token;
String openid;
String scope;
}
<div class="md-section-divider"></div>
@Data
public 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;
}