28 changed files with 1482 additions and 83 deletions
@ -0,0 +1,151 @@ |
|||
package com.ccsens.util.bean.wx.dto; |
|||
|
|||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; |
|||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
@JacksonXmlRootElement(localName = "xml") |
|||
public class WxGzhAction { |
|||
@Data |
|||
private static class ScanCodeInfo{ |
|||
@JacksonXmlProperty(localName = "ScanType") |
|||
private String scanType; |
|||
@JacksonXmlProperty(localName = "ScanResult") |
|||
private String scanResult; |
|||
}; |
|||
|
|||
@Data |
|||
private static class SendPicsInfo{ |
|||
@Data |
|||
private static class Item{ |
|||
@JacksonXmlProperty(localName = "PicMd5Sum") |
|||
private String picMd5Sum; |
|||
} |
|||
@JacksonXmlProperty(localName = "Count") |
|||
private Integer count; |
|||
@JacksonXmlProperty(localName = "PicList") |
|||
private List<Item> picList; |
|||
} |
|||
|
|||
@Data |
|||
private static class SendLocationInfo{ |
|||
@JacksonXmlProperty(localName = "Location_X") |
|||
private String x; |
|||
@JacksonXmlProperty(localName = "Location_Y") |
|||
private String y; |
|||
@JacksonXmlProperty(localName = "Scale") |
|||
private String scale; |
|||
@JacksonXmlProperty(localName = "Label") |
|||
private String label; |
|||
@JacksonXmlProperty(localName = "Poiname") |
|||
private String poiname; |
|||
} |
|||
|
|||
@JacksonXmlProperty(localName = "FromUserName") |
|||
private String openId; |
|||
@JacksonXmlProperty(localName = "ToUserName") |
|||
private String gzhId; |
|||
@JacksonXmlProperty(localName = "CreateTime") |
|||
private Long createTime; |
|||
@JacksonXmlProperty(localName = "MsgType") |
|||
private String msgType; |
|||
|
|||
/** |
|||
* MsgType: Text (Content,MsgId) |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Content") |
|||
private String content; |
|||
@JacksonXmlProperty(localName = "MsgId") |
|||
private String msgId; |
|||
/** |
|||
* MsgType: image (PicUrl,MediaId,MsgId) |
|||
*/ |
|||
@JacksonXmlProperty(localName = "PicUrl") |
|||
private String picUrl; |
|||
@JacksonXmlProperty(localName = "MediaId") |
|||
private String mediaId; |
|||
/** |
|||
* MsgType: voice (MediaId,Format,MsgId) |
|||
* Recognition: 开通语音识别后生效 |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Format") |
|||
private String format; |
|||
@JacksonXmlProperty(localName = "Recognition") |
|||
private String recognition; |
|||
|
|||
/** |
|||
* MsgType: video/shortvideo(MediaId,ThumbMediaId,MsgId) |
|||
*/ |
|||
@JacksonXmlProperty(localName = "ThumbMediaId") |
|||
private String thumbMediaId; |
|||
/** |
|||
* MsgType: location(Location_x,Location_y,Scale,Label,MsgId) |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Location_X") |
|||
private String x; |
|||
@JacksonXmlProperty(localName = "Location_Y") |
|||
private String y; |
|||
@JacksonXmlProperty(localName = "Scale") |
|||
private String scale; |
|||
@JacksonXmlProperty(localName = "Label") |
|||
private String label; |
|||
/** |
|||
* MsgType: link(Title,Description,Url,MsgId) |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Title") |
|||
private String title; |
|||
@JacksonXmlProperty(localName = "Description") |
|||
private String description; |
|||
@JacksonXmlProperty(localName = "Url") |
|||
private String url; |
|||
|
|||
/** |
|||
* MsgType: event |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Event") |
|||
private String event; |
|||
@JacksonXmlProperty(localName = "EventKey") |
|||
private String eventKey; |
|||
|
|||
/** |
|||
* Event: subscribe/SCAN |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Ticket") |
|||
private String ticket; |
|||
/** |
|||
* Event: Location |
|||
*/ |
|||
@JacksonXmlProperty(localName = "Latitude") |
|||
private String latitude; |
|||
@JacksonXmlProperty(localName = "Longitude") |
|||
private String longitude; |
|||
@JacksonXmlProperty(localName = "Precision") |
|||
private String precision; |
|||
|
|||
/** |
|||
* Event: VIEW/view_miniprogram |
|||
*/ |
|||
@JacksonXmlProperty(localName = "MenuId") |
|||
private String menuId; |
|||
/** |
|||
* Event: scancode_push/scancode_waitmsg |
|||
*/ |
|||
@JacksonXmlProperty(localName = "ScanCodeInfo") |
|||
private ScanCodeInfo scanCodeInfo; |
|||
/** |
|||
* Event: pic_sysphoto/pic_photo_or_album/pic_weixin |
|||
*/ |
|||
@JacksonXmlProperty(localName = "SendPicsInfo") |
|||
private SendPicsInfo sendPicsInfo; |
|||
/** |
|||
* Event: location_select |
|||
*/ |
|||
@JacksonXmlProperty(localName = "SendLocationInfo") |
|||
private SendLocationInfo sendLocationInfo; |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
public class WxAccessToken extends WxBaseEntity{ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
@JsonProperty("expires_in") |
|||
private Long expiresIn; |
|||
@JsonIgnore |
|||
private Long createdAt; |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
public class WxBaseEntity { |
|||
private Integer errcode; |
|||
private String errmsg; |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
|
|||
public enum WxGzhAuthType { |
|||
//基本授权类型,静默,只能获取openId
|
|||
SNSAPI_BASE("snsapi_base"), |
|||
//信息授权类型,弹出提示框,可以获取openId,unionId,nickanme,city等用户信息
|
|||
SNSAPI_USERINFO("snsapi_userinfo"); |
|||
|
|||
@Getter |
|||
@Setter |
|||
private String text; |
|||
|
|||
private WxGzhAuthType(String text){ |
|||
this.text = text; |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.Getter; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
public class WxGzhMenu { |
|||
public enum ButtonType{ |
|||
//点击推事件
|
|||
CLICK("click"), |
|||
//跳转URL
|
|||
VIEW("view"), |
|||
//小程序
|
|||
MINIPROGRAM("miniprogram"), |
|||
//扫码推事件
|
|||
SCANCODE_PUSH("scancode_push"), |
|||
//扫码推事件且弹出“消息接收中”提示框
|
|||
SCANCODE_WAITMSG("scancode_waitmsg"), |
|||
//弹出系统拍照发图
|
|||
PIC_SYSPHOTO("pic_sysphoto"), |
|||
//弹出拍照或者相册发图
|
|||
PIC_PHOTO_OR_ALBUM("pic_photo_or_album"), |
|||
//弹出微信相册发图器
|
|||
PIC_WEIXIN("pic_weixin"), |
|||
//弹出地理位置选择器
|
|||
LOCATION_SELECT("location_select"), |
|||
//下发消息(除文本消息)
|
|||
MEDIA_ID("media_id"), |
|||
//跳转图文消息URL
|
|||
VIEW_LIMITED("view_limited"); |
|||
|
|||
@JsonValue |
|||
private String text; |
|||
|
|||
ButtonType(String text){ |
|||
this.text = text; |
|||
} |
|||
} |
|||
@Builder(toBuilder = true) |
|||
@Getter |
|||
public static class Button{ |
|||
/** |
|||
* 必须 |
|||
*/ |
|||
private ButtonType type; |
|||
/** |
|||
* 必须 |
|||
*/ |
|||
private String name; |
|||
/** |
|||
* click等点击类型必须 |
|||
*/ |
|||
private String key; |
|||
/** |
|||
* view、miniprogram类型必须 |
|||
*/ |
|||
private String url; |
|||
/** |
|||
* media_id类型和view_limited类型必须 |
|||
*/ |
|||
@JsonProperty("media_id") |
|||
private String mediaId; |
|||
/** |
|||
* miniprogram类型必须 |
|||
*/ |
|||
private String appid; |
|||
private String pagepath; |
|||
/** |
|||
* optional |
|||
*/ |
|||
@JsonProperty("sub_button") |
|||
private List<Button> subButton; |
|||
} |
|||
@JsonProperty("button") |
|||
private List<Button> buttons; |
|||
|
|||
public static WxGzhMenu newInstance(){ |
|||
return new WxGzhMenu(); |
|||
} |
|||
|
|||
public WxGzhMenu add(Button button){ |
|||
if(buttons == null){ |
|||
buttons = CollectionUtil.newArrayList(); |
|||
} |
|||
buttons.add(button); |
|||
return this; |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
public enum WxGzhMsgEvent { |
|||
/** |
|||
* 关注/取消关注事件 |
|||
*/ |
|||
SUBSCRIBE("subscribe"),UNSUBSCRIBE("unsubscribe"), |
|||
/** |
|||
* 扫描带参数(公众号)二维码事件 |
|||
* 未关注:产生Event: SUBSCRIBE EventKey: qrscene_xxx |
|||
* 已关注:产生Event: Scan EventKey: scene_value |
|||
*/ |
|||
SCAN("SCAN"), |
|||
/** |
|||
* 上报地理位置事件 |
|||
*/ |
|||
LOCATION("LOCATION"), |
|||
/** |
|||
* 自定义菜单事件 |
|||
*/ |
|||
//点击菜单拉取消息时的事件推送
|
|||
CLICK("CLICK"), |
|||
//点击菜单跳转链接时的事件推送
|
|||
VIEW("VIEW"), |
|||
//扫码推事件的事件推送
|
|||
SCANCODE_PUSH("scancode_push"), |
|||
//扫码推事件且弹出“消息接收中”提示框的事件推送
|
|||
SCANCODE_WAITMSG("scancode_waitmsg"), |
|||
//弹出系统拍照发图的事件推送
|
|||
PIC_SYSPHOTO("pic_sysphoto"), |
|||
//弹出拍照或者相册发图的事件推送
|
|||
PIC_PHOTO_OR_ALBUM("pic_photo_or_album"), |
|||
//弹出微信相册发图器的事件推送
|
|||
PIC_WEIXIN("pic_weixin"), |
|||
//弹出地理位置选择器的事件推送
|
|||
LOCATION_SELECT("location_select"), |
|||
//点击菜单跳转小程序的事件推送
|
|||
VIEW_MINIPROGRAM("view_miniprogram"); |
|||
|
|||
@Getter |
|||
@Setter |
|||
private String text; |
|||
|
|||
private WxGzhMsgEvent(String text){ |
|||
this.text = text; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
public enum WxGzhMsgType { |
|||
/** |
|||
* 1.普通消息 |
|||
*/ |
|||
//1.1 文本消息
|
|||
TEXT("text"), |
|||
IMAGE("image"), |
|||
VOICE("voice"), |
|||
VIDEO("video"), |
|||
SHORTVIDEO("shortvideo"), |
|||
LOCATION("location"), |
|||
LINK("link"), |
|||
|
|||
/** |
|||
* 2.事件推送 |
|||
*/ |
|||
EVENT("event"); |
|||
|
|||
@Getter |
|||
@Setter |
|||
private String text; |
|||
|
|||
private WxGzhMsgType(String text){ |
|||
this.text = text; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
public class WxOauth2AccessToken extends WxBaseEntity{ |
|||
@JsonProperty("access_token") |
|||
private String accessToken; |
|||
@JsonProperty("expires_in") |
|||
private int expiresIn; |
|||
@JsonProperty("refresh_token") |
|||
private String refreshToken; |
|||
@JsonProperty("openid") |
|||
private String openId; |
|||
@JsonProperty("scope") |
|||
private String scope; |
|||
@JsonProperty("unionid") |
|||
private String unionId; |
|||
@JsonIgnore |
|||
private Long createdAt; |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
public class WxOauth2UserInfo extends WxBaseEntity{ |
|||
@JsonProperty("openid") |
|||
private String openId; |
|||
|
|||
@JsonProperty("nickname") |
|||
private String nickname; |
|||
|
|||
@JsonProperty("sex") |
|||
private int sex; |
|||
|
|||
@JsonProperty("province") |
|||
private String province; |
|||
|
|||
@JsonProperty("city") |
|||
private String city; |
|||
|
|||
@JsonProperty("country") |
|||
private String country; |
|||
|
|||
@JsonProperty("headimgurl") |
|||
private String headImgUrl; |
|||
|
|||
@JsonProperty("privilege") |
|||
private List<String> privilegeList; |
|||
|
|||
@JsonProperty("unionid") |
|||
private String unionId; |
|||
|
|||
@JsonProperty("language") |
|||
private String language; |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.ccsens.util.bean.wx.po; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
|
|||
public enum WxOperation { |
|||
/** |
|||
* 创建公众号底部菜单 |
|||
*/ |
|||
CREATE_MENU("CreateMenu"); |
|||
|
|||
@JsonValue |
|||
private String text; |
|||
|
|||
WxOperation(String text){ |
|||
this.text = text; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.ccsens.util.exception; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
public class BusinessException extends BaseException { |
|||
private String type = "BusinessError"; |
|||
|
|||
public BusinessException(){ |
|||
|
|||
} |
|||
|
|||
public BusinessException(Integer errcode, String errmsg){ |
|||
super(errcode,errmsg); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.ccsens.util.exception; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
@Data |
|||
public class WxException extends BaseException{ |
|||
private String type = "WxError"; |
|||
|
|||
public WxException(){ |
|||
|
|||
} |
|||
|
|||
public WxException(Integer errcode, String errmsg){ |
|||
super(errcode,errmsg); |
|||
} |
|||
} |
|||
@ -0,0 +1,273 @@ |
|||
package com.ccsens.util.wx; |
|||
|
|||
import cn.hutool.core.lang.Console; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.core.util.URLUtil; |
|||
import cn.hutool.http.HttpRequest; |
|||
|
|||
import com.ccsens.util.DateUtil; |
|||
import com.ccsens.util.JacksonUtil; |
|||
import com.ccsens.util.bean.wx.po.*; |
|||
import com.ccsens.util.exception.BaseException; |
|||
import com.ccsens.util.exception.BusinessException; |
|||
import com.ccsens.util.exception.WxException; |
|||
import com.fasterxml.jackson.core.JsonProcessingException; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.io.IOException; |
|||
import java.security.MessageDigest; |
|||
import java.util.Arrays; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
public class WxGzhUtil { |
|||
private static final Logger logger = LoggerFactory.getLogger(WxGzhUtil.class); |
|||
/** |
|||
* 全局唯一accessToken |
|||
*/ |
|||
private static WxAccessToken globalWxAccessToken; |
|||
/** |
|||
* accessToken 过期提前10分钟刷新 |
|||
*/ |
|||
private static final Integer ACCESS_TOKEN_RESERVED_SECONDS = 10 * 60; |
|||
|
|||
private static final String URL_LOGIN |
|||
= "https://api.weixin.qq.com/sns/jscode2session?appid=%1$s&secret=%2$s&js_code=%3$s&grant_type=%4$s"; |
|||
private static final String URL_GET_ACCESS_TOKEN |
|||
= "https://api.weixin.qq.com/cgi-bin/token?grant_type=%1$s&appid=%2$s&secret=%3$s"; |
|||
private static final String URL_GET_WX_CODE_A |
|||
= "https://api.weixin.qq.com/wxa/getwxacode?access_token=%1$s"; |
|||
private static final String URL_GET_WX_CODE_B |
|||
= "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%1$s"; |
|||
private static final String URL_GET_WX_CODE_C |
|||
= "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=%1$s"; |
|||
private static final String URL_PREPARE_PAY |
|||
= "https://api.mch.weixin.qq.com/pay/unifiedorder"; |
|||
private static final String URL_PAY_TO_USR_WX |
|||
= "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; |
|||
private static final String URL_PAY_TO_USR_BANK |
|||
= "https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank"; |
|||
private static final String URL_CREATE_MENU |
|||
= " https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%1$s"; |
|||
private static final String URL_QUERY_MENU |
|||
= "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=%1$s"; |
|||
private static final String GZH_AUTH_URL |
|||
= "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%1$s&redirect_uri=%2$s&response_type=code&scope=%3$s&state=STATE#wechat_redirect"; |
|||
private static final String URL_GET_OAUTH2_ACCESS_TOKEN |
|||
= "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%1$s&secret=%2$s&code=%3$s&grant_type=authorization_code"; |
|||
private static final String URL_GET_OAUTH2_USERINFO |
|||
= "https://api.weixin.qq.com/sns/userinfo?access_token=%1$s&openid=%2$s"; |
|||
private static final String APPID = "wx7af1bf1e14facf82"; |
|||
private static final String SECRET = "a6613fae11b497639c0224b820aaf6d9"; |
|||
private static final String TOKEN = "nNzkL9KkZUOIS8uU"; |
|||
private static final String ENCODING_AES_KEY = "MQEXG7grhRNsARbUzem6OwnGr2ZW9o5jsauNqaQWOuu"; |
|||
private static final String MCHID = ""; |
|||
private static final String KEY = ""; |
|||
|
|||
/** |
|||
* 数组转字符串 |
|||
*/ |
|||
private static String arrayToString(String[] arr) { |
|||
StringBuilder bf = new StringBuilder(); |
|||
for (String s : arr) { |
|||
bf.append(s); |
|||
} |
|||
return bf.toString(); |
|||
} |
|||
|
|||
/** |
|||
* sha1加密 |
|||
*/ |
|||
private static String sha1Encode(String sourceString) { |
|||
String resultString = null; |
|||
try { |
|||
MessageDigest md = MessageDigest.getInstance("SHA-1"); |
|||
resultString = byte2hexString(md.digest(sourceString.getBytes())); |
|||
} catch (Exception ex) { |
|||
ex.printStackTrace(); |
|||
} |
|||
return resultString; |
|||
} |
|||
|
|||
/** |
|||
* 字节数组转字符串 |
|||
*/ |
|||
private static String byte2hexString(byte[] bytes) { |
|||
StringBuilder buf = new StringBuilder(bytes.length * 2); |
|||
for (byte aByte : bytes) { |
|||
if (((int) aByte & 0xff) < 0x10) { |
|||
buf.append("0"); |
|||
} |
|||
buf.append(Long.toString((int) aByte & 0xff, 16)); |
|||
} |
|||
return buf.toString().toUpperCase(); |
|||
} |
|||
|
|||
/** |
|||
* 公众号接入验证 |
|||
* @param signature signature |
|||
* @param timestamp timestamp |
|||
* @param nonce nonce |
|||
* @param echostr echostr |
|||
* @return true or false |
|||
*/ |
|||
public static boolean checkSignature(String signature,String timestamp,String nonce,String echostr){ |
|||
if(StrUtil.isEmpty(signature) || StrUtil.isEmpty(timestamp) || StrUtil.isEmpty(nonce) || StrUtil.isEmpty(echostr)){ |
|||
return false; |
|||
} |
|||
String[] tmpArr = { TOKEN, timestamp, nonce }; |
|||
Arrays.sort(tmpArr); |
|||
String sign = sha1Encode( arrayToString(tmpArr)); |
|||
Console.log("sign:{} , signature: {}",sign,signature); |
|||
if (sign.equalsIgnoreCase(signature)) { |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* 获取Access_token |
|||
*/ |
|||
public static String getAccessToken() throws BaseException { |
|||
if(globalWxAccessToken == null || |
|||
globalWxAccessToken.getCreatedAt() + globalWxAccessToken.getExpiresIn() |
|||
>= DateUtil.currentSeconds() - ACCESS_TOKEN_RESERVED_SECONDS){ |
|||
WxAccessToken wxAccessToken = null; |
|||
String url = String.format(URL_GET_ACCESS_TOKEN,"client_credential",APPID,SECRET); |
|||
String response = HttpRequest.get(url).execute().body(); |
|||
Console.log("getAccessToken: {}",response); |
|||
try { |
|||
if(StrUtil.isEmpty(response) || null == (wxAccessToken = JacksonUtil.jsonToBean(response, WxAccessToken.class))) { |
|||
throw new BusinessException(-1,"the response of HttpRequest is empty."); |
|||
} |
|||
} catch (IOException e) { |
|||
throw new BusinessException(-1,e.getMessage()); |
|||
} |
|||
|
|||
if(null != wxAccessToken.getErrcode()){ |
|||
throw new WxException(wxAccessToken.getErrcode(),wxAccessToken.getErrmsg()); |
|||
} |
|||
if (StrUtil.isEmpty(wxAccessToken.getAccessToken())) { |
|||
throw new BusinessException(-1,"can't find the access_token attribute."); |
|||
} |
|||
globalWxAccessToken = wxAccessToken; |
|||
globalWxAccessToken.setCreatedAt(DateUtil.currentSeconds()); |
|||
} |
|||
return globalWxAccessToken.getAccessToken(); |
|||
} |
|||
|
|||
/** |
|||
* 查询微信公众号底部菜单 |
|||
*/ |
|||
public static String queryMenu() throws BaseException { |
|||
String url = String.format(URL_QUERY_MENU,getAccessToken()); |
|||
String menuString = HttpRequest.get(url).execute().body(); |
|||
Console.log("url:{}, menuString: {}",url,menuString); |
|||
return menuString; |
|||
} |
|||
|
|||
/** |
|||
* 创建微信公众号底部菜单 |
|||
*/ |
|||
public static void createMenu(WxGzhMenu gzhMenu) throws BaseException { |
|||
String url = String.format(URL_CREATE_MENU,getAccessToken()); |
|||
String menuString = null; |
|||
try { |
|||
menuString = JacksonUtil.beanToJson(gzhMenu); |
|||
Console.log(menuString); |
|||
} catch (JsonProcessingException e) { |
|||
e.printStackTrace(); |
|||
throw new BusinessException(-1,e.getMessage()); |
|||
} |
|||
String response = HttpRequest.post(url).body(menuString).execute().body(); |
|||
if(StrUtil.isEmpty(response)){ |
|||
throw new BusinessException(-1,"the response of HttpRequest is empty."); |
|||
} |
|||
Map<String,Object> map = null; |
|||
try { |
|||
map = JacksonUtil.jsonToMap(response); |
|||
} catch (IOException e) { |
|||
throw new BusinessException(-1,e.getMessage()); |
|||
} |
|||
|
|||
Object errcode = map.get("errcode"); |
|||
if(errcode == null || ((Integer)errcode) != 0) { |
|||
throw new WxException((Integer)errcode,(String)map.get("errmsg")); |
|||
} |
|||
} |
|||
|
|||
public static String getAuthedUrl(String url, WxGzhAuthType wxGzhAuthType){ |
|||
return String.format(GZH_AUTH_URL,APPID, URLUtil.encode(url),wxGzhAuthType.getText()); |
|||
} |
|||
|
|||
/** |
|||
* 获取网页授权凭证 |
|||
* @param code OAuth2授权码 |
|||
* @return WxOauth2AccessToken |
|||
*/ |
|||
public static WxOauth2AccessToken getOauth2AccessToken(String code) throws BaseException { |
|||
WxOauth2AccessToken wxOauth2AccessToken = null; |
|||
String url = String.format(URL_GET_OAUTH2_ACCESS_TOKEN,APPID,SECRET,code); |
|||
String response = HttpRequest.get(url).execute().body(); |
|||
Console.log("url: {}\nresponse: {}",url,response); |
|||
try { |
|||
if(StrUtil.isEmpty(response) || null == (wxOauth2AccessToken = JacksonUtil.jsonToBean(response,WxOauth2AccessToken.class))) { |
|||
throw new BusinessException(-1,"the response of HttpRequest is empty."); |
|||
} |
|||
} catch (IOException e) { |
|||
throw new BusinessException(-1,e.getMessage()); |
|||
} |
|||
|
|||
if(null != wxOauth2AccessToken.getErrcode()){ |
|||
throw new WxException(wxOauth2AccessToken.getErrcode(),wxOauth2AccessToken.getErrmsg()); |
|||
} |
|||
if (StrUtil.isEmpty(wxOauth2AccessToken.getAccessToken())) { |
|||
throw new BusinessException(-1,"can't find the access_token attribute."); |
|||
} |
|||
wxOauth2AccessToken.setCreatedAt(DateUtil.currentSeconds()); |
|||
return wxOauth2AccessToken; |
|||
} |
|||
|
|||
/** |
|||
* 通过网页授权获取用户信息 |
|||
* |
|||
* @param accessToken 网页授权接口调用凭证 |
|||
* @param openId 用户标识 |
|||
* @return SNSUserInfo |
|||
*/ |
|||
public static WxOauth2UserInfo getOauth2UserInfo(String accessToken, String openId) throws BaseException { |
|||
WxOauth2UserInfo wxOauth2UserInfo = null; |
|||
String url = String.format(URL_GET_OAUTH2_USERINFO,accessToken,openId); |
|||
String response = HttpRequest.get(url).execute().body(); |
|||
Console.log("url: {}\nresponse: {}",url,response); |
|||
try { |
|||
if(StrUtil.isEmpty(response) || null == (wxOauth2UserInfo = JacksonUtil.jsonToBean(response,WxOauth2UserInfo.class))) { |
|||
throw new BusinessException(-1,"the response of HttpRequest is empty."); |
|||
} |
|||
} catch (IOException e) { |
|||
throw new BusinessException(-1,e.getMessage()); |
|||
} |
|||
|
|||
if(null != wxOauth2UserInfo.getErrcode()){ |
|||
throw new WxException(wxOauth2UserInfo.getErrcode(),wxOauth2UserInfo.getErrmsg()); |
|||
} |
|||
if (StrUtil.isEmpty(wxOauth2UserInfo.getOpenId())) { |
|||
throw new BusinessException(-1,"can't find the openid attribute."); |
|||
} |
|||
return wxOauth2UserInfo; |
|||
} |
|||
|
|||
/** |
|||
* 通过网页授权code拉取用户信息,封装了getOauth2AccessToken和getOauth2UserInfo(accessToken,openId) |
|||
* @param code 网页授权码 |
|||
* @return WxOauth2UserInfo |
|||
* @throws BaseException 异常 |
|||
*/ |
|||
public static WxOauth2UserInfo getOauth2UserInfo(String code) throws BaseException { |
|||
WxOauth2AccessToken wxOauth2AccessToken = getOauth2AccessToken(code); |
|||
return getOauth2UserInfo(wxOauth2AccessToken.getAccessToken(),wxOauth2AccessToken.getOpenId()); |
|||
} |
|||
} |
|||
@ -0,0 +1,406 @@ |
|||
package com.ccsens.util.wx; |
|||
|
|||
import cn.hutool.core.util.ObjectUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.http.HttpRequest; |
|||
|
|||
import com.ccsens.util.HttpsUtil; |
|||
import com.ccsens.util.JacksonUtil; |
|||
import com.ccsens.util.WebConstant; |
|||
import com.ccsens.util.exception.BusinessException; |
|||
import com.ccsens.util.exception.WxException; |
|||
import lombok.Data; |
|||
import org.apache.commons.codec.digest.DigestUtils; |
|||
|
|||
import java.io.ByteArrayInputStream; |
|||
import java.io.IOException; |
|||
import java.io.UnsupportedEncodingException; |
|||
import java.net.ConnectException; |
|||
import java.util.*; |
|||
|
|||
@Data |
|||
class GetUserInfoException extends RuntimeException{ |
|||
private int code; |
|||
public GetUserInfoException(int code,String message){ |
|||
super(message); |
|||
this.code = code; |
|||
} |
|||
} |
|||
|
|||
@Data |
|||
class PayException extends RuntimeException{ |
|||
private int code; |
|||
public PayException(int code,String message){ |
|||
super(message); |
|||
this.code = code; |
|||
} |
|||
|
|||
public PayException(String message){ |
|||
super(message); |
|||
this.code = -1; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @author __zHangSan |
|||
*/ |
|||
public class WxXcxUtil { |
|||
|
|||
/** |
|||
* 被Jasckson使用的内部类 必须是static的 |
|||
*/ |
|||
public static class WechatUser { |
|||
public String openid; |
|||
public String session_key; |
|||
public String unionid; |
|||
public Integer errcode; |
|||
public String errmsg; |
|||
} |
|||
|
|||
/** |
|||
* Get wx code |
|||
*/ |
|||
public static class WechatCode{ |
|||
public String scene; |
|||
public String page; |
|||
public Integer width; |
|||
public Boolean auto_color; |
|||
public Boolean line_color; |
|||
public Boolean is_hyaline; |
|||
} |
|||
|
|||
/** |
|||
* Prepare pay |
|||
*/ |
|||
public static class PreparePayBean{ |
|||
public String appid;//小程序ID
|
|||
public String mch_id;//商户号
|
|||
public String device_info;//设备号
|
|||
public String nonce_str;//随机字符串
|
|||
public String sign;//签名
|
|||
public String sign_type; //签名类型
|
|||
public String body;//商品描述
|
|||
public String detail;//商品详情
|
|||
public String attach;//附加数据
|
|||
public String out_trade_no;//商户订单号
|
|||
public String fee_type;//货币类型
|
|||
public Integer total_fee; //金额(分)
|
|||
public String spbill_create_ip;//终端IP
|
|||
public String time_start;//交易起始时间
|
|||
public String time_expire;//交易结束时间
|
|||
public String goods_tag;//商品标记
|
|||
public String notify_url;//通知地址
|
|||
public String trade_type;//交易类型
|
|||
public String product_id;//商品id
|
|||
public String limit_pay;//指定支付方式
|
|||
public String openid;//用户标识
|
|||
} |
|||
|
|||
/** |
|||
* Prepare pay result |
|||
*/ |
|||
public static class PreparePayResultBean{ |
|||
public String return_code; |
|||
public String return_msg; |
|||
public String result_code; |
|||
public String prepay_id; |
|||
public String err_code; |
|||
public String err_code_des; |
|||
} |
|||
|
|||
/** |
|||
* Prepare pay resign |
|||
*/ |
|||
public static class PreparePayReSignBean{ |
|||
public String nonceStr; |
|||
public String _package; |
|||
public long timeStamp; |
|||
public String paySign; |
|||
public String signType; |
|||
} |
|||
|
|||
private static final String URL_LOGIN |
|||
= "https://api.weixin.qq.com/sns/jscode2session?appid=%1$s&secret=%2$s&js_code=%3$s&grant_type=%4$s"; |
|||
private static final String URL_GET_ACCESS_TOKEN |
|||
= "https://api.weixin.qq.com/cgi-bin/token?grant_type=%1$s&appid=%2$s&secret=%3$s"; |
|||
private static final String URL_GET_WX_CODE_A |
|||
= "https://api.weixin.qq.com/wxa/getwxacode?access_token=%1$s"; |
|||
private static final String URL_GET_WX_CODE_B |
|||
= "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%1$s"; |
|||
private static final String URL_GET_WX_CODE_C |
|||
= "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=%1$s"; |
|||
private static final String URL_PREPARE_PAY |
|||
= "https://api.mch.weixin.qq.com/pay/unifiedorder"; |
|||
private static final String URL_PAY_TO_USR_WX |
|||
= "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; |
|||
private static final String URL_PAY_TO_USR_BANK |
|||
= "https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank"; |
|||
private static final String URL_CREATE_MENU |
|||
= " https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%1$s"; |
|||
|
|||
private static final String appid = "wx7af1bf1e14facf82"; |
|||
private static final String secret = "a6613fae11b497639c0224b820aaf6d9"; |
|||
private static final String mchid = ""; |
|||
private static final String key = ""; |
|||
|
|||
/** |
|||
* 小程序登陆,根据code获取用户信息(openid,sessionKey,) |
|||
* @param code |
|||
* @return |
|||
* @throws ConnectException |
|||
* @throws Exception |
|||
*/ |
|||
public static WechatUser getUserInfo(String code) throws Exception { |
|||
WechatUser wechatUser = null; |
|||
String url = String.format(URL_LOGIN, appid, secret, code, "authorization_code"); |
|||
String response = HttpRequest.get(url).execute().body(); |
|||
try { |
|||
if(StrUtil.isEmpty(response) || null == (wechatUser = JacksonUtil.jsonToBean(response, WechatUser.class))) { |
|||
throw new BusinessException(-1,"the response of HttpRequest is empty."); |
|||
} |
|||
} catch (IOException e) { |
|||
throw new BusinessException(-1,e.getMessage()); |
|||
} |
|||
|
|||
if(null != wechatUser.errcode){ |
|||
throw new WxException(wechatUser.errcode, wechatUser.errmsg); |
|||
} |
|||
return wechatUser; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取小程序二维码/小程序码 |
|||
* @return |
|||
* @throws Exception |
|||
*/ |
|||
public static void getWxCode(String page,String scene,String path) throws Exception { |
|||
String url = String.format(URL_GET_WX_CODE_B, WxGzhUtil.getAccessToken()); |
|||
|
|||
WechatCode wechatCode = new WechatCode(); |
|||
wechatCode.page = page; |
|||
wechatCode.scene = scene; |
|||
|
|||
String postStr = JacksonUtil.beanToJson(wechatCode); |
|||
System.out.println(postStr); |
|||
|
|||
HttpsUtil.httpsRequest(url,"POST",postStr,path); |
|||
} |
|||
|
|||
/** |
|||
* 统一下单 |
|||
*/ |
|||
public static PreparePayReSignBean preparePay(String openid,String out_trade_no,String body,int total_fee,String spbill_create_ip, |
|||
String notify_url) throws Exception { |
|||
PreparePayBean preparePayBean = new PreparePayBean(); |
|||
preparePayBean.appid = appid; |
|||
preparePayBean.mch_id = mchid; |
|||
preparePayBean.openid = openid; |
|||
preparePayBean.total_fee = total_fee; |
|||
preparePayBean.out_trade_no = out_trade_no; |
|||
//以utf-8编码放入paymentPo,微信支付要求字符编码统一采用UTF-8字符编码
|
|||
preparePayBean.body = new String(body.getBytes("ISO-8859-1"),"UTF-8"); |
|||
//preparePayBean.body = new String("小程序-充值");
|
|||
preparePayBean.spbill_create_ip = spbill_create_ip; |
|||
preparePayBean.notify_url = notify_url; |
|||
preparePayBean.trade_type = "JSAPI"; |
|||
preparePayBean.nonce_str = createNonceStr(); |
|||
|
|||
// 把请求参数打包成数组
|
|||
Map<String,Object> sParaTemp = new HashMap(); |
|||
sParaTemp.put("appid", preparePayBean.appid); |
|||
sParaTemp.put("mch_id", preparePayBean.mch_id); |
|||
sParaTemp.put("openid", preparePayBean.openid); |
|||
sParaTemp.put("total_fee",preparePayBean.total_fee + ""); |
|||
sParaTemp.put("out_trade_no", preparePayBean.out_trade_no); |
|||
sParaTemp.put("body", preparePayBean.body); |
|||
sParaTemp.put("spbill_create_ip", preparePayBean.spbill_create_ip); |
|||
sParaTemp.put("notify_url",preparePayBean.notify_url); |
|||
sParaTemp.put("trade_type", preparePayBean.trade_type); |
|||
sParaTemp.put("nonce_str", preparePayBean.nonce_str); |
|||
|
|||
preparePayBean.sign = createSign(sParaTemp); |
|||
|
|||
String reqrXml = JacksonUtil.beanToXml(preparePayBean,"xml"); |
|||
String respXml = HttpsUtil.httpsRequest(URL_PREPARE_PAY,"POST",reqrXml); |
|||
|
|||
System.out.println("---------------PreparePay Request Xml-----------------"); |
|||
System.out.println(reqrXml); |
|||
System.out.println("---------------PreparePay Response Xml-----------------"); |
|||
System.out.println(respXml); |
|||
System.out.println("---------------PreparePay end-----------------"); |
|||
|
|||
PreparePayResultBean resultBean = null; |
|||
PreparePayReSignBean reSignBean = new PreparePayReSignBean(); |
|||
resultBean = JacksonUtil.xmlToBean(new ByteArrayInputStream(respXml.getBytes()),PreparePayResultBean.class); |
|||
|
|||
if(ObjectUtil.isNull(resultBean)) { |
|||
throw new PayException("PreparePay 返回的resultBean为空"); |
|||
} |
|||
if(!StrUtil.isEmpty(resultBean.return_code) |
|||
&& !StrUtil.isEmpty(resultBean.result_code)){ |
|||
if(resultBean.result_code.equals("SUCCESS") && resultBean.return_code.equals(resultBean.result_code)){ |
|||
reSignBean.nonceStr = createNonceStr(); |
|||
reSignBean._package = "prepay_id="+resultBean.prepay_id; |
|||
reSignBean.timeStamp = System.currentTimeMillis() / 1000; |
|||
reSignBean.signType = "MD5"; |
|||
|
|||
// 把请求参数打包成数组
|
|||
sParaTemp = new HashMap(); |
|||
sParaTemp.put("appId", appid); |
|||
sParaTemp.put("package", reSignBean._package); |
|||
sParaTemp.put("nonceStr", reSignBean.nonceStr); |
|||
sParaTemp.put("signType", reSignBean.signType); |
|||
sParaTemp.put("timeStamp",reSignBean.timeStamp + ""); |
|||
|
|||
reSignBean.paySign = createSign(sParaTemp); |
|||
|
|||
return reSignBean; |
|||
}else{ |
|||
throw new PayException("[" + resultBean.err_code + "]" |
|||
+ resultBean.err_code_des); |
|||
} |
|||
}else { |
|||
throw new PayException(resultBean.return_msg); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create Sign |
|||
* @param sParaTemp |
|||
* @return |
|||
*/ |
|||
public static String createSign(Map<String,Object> sParaTemp){ |
|||
// 除去数组中的空值和签名参数
|
|||
Map sPara = paraFilter(sParaTemp); |
|||
// 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串
|
|||
String prestr = createLinkString(sPara); |
|||
//MD5运算生成签名
|
|||
String sign = sign(prestr, key, "utf-8").toUpperCase(); |
|||
return sign; |
|||
} |
|||
|
|||
/** |
|||
* 除去数组中的空值和签名参数 |
|||
* @param sArray 签名参数组 |
|||
* @return 去掉空值与签名参数后的新签名参数组 |
|||
*/ |
|||
private static Map paraFilter(Map<String,Object> sArray) { |
|||
Map result = new HashMap(); |
|||
if (sArray == null || sArray.size() <= 0) { |
|||
return result; |
|||
} |
|||
for (String key : sArray.keySet()) { |
|||
String value = "" + sArray.get(key); |
|||
if (StrUtil.isEmpty(value) || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) { |
|||
continue; |
|||
} |
|||
result.put(key, value); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串 |
|||
* @param params 需要排序并参与字符拼接的参数组 |
|||
* @return 拼接后字符串 |
|||
*/ |
|||
private static String createLinkString(Map<String,String> params) { |
|||
List<String> keys = new ArrayList<String>(params.keySet()); |
|||
Collections.sort(keys); |
|||
StringBuilder prestr = new StringBuilder(); |
|||
for (int i = 0; i < keys.size(); i++) { |
|||
String key = keys.get(i); |
|||
String value = params.get(key); |
|||
if (i == keys.size() - 1) { |
|||
prestr.append(key).append("=").append(value); |
|||
} else { |
|||
prestr.append(key).append("=").append(value).append("&"); |
|||
} |
|||
} |
|||
return prestr.toString(); |
|||
} |
|||
|
|||
/** |
|||
* 签名字符串 |
|||
* @param text 需要签名的字符串 |
|||
* @param key 密钥 |
|||
* @param input_charset 编码格式 |
|||
* @return 签名结果 |
|||
*/ |
|||
private static String sign(String text, String key, String input_charset) { |
|||
text = text + "&key=" + key; |
|||
return DigestUtils.md5Hex(getContentBytes(text, input_charset)); |
|||
} |
|||
|
|||
/** |
|||
* getContentBytes |
|||
*/ |
|||
private static byte[] getContentBytes(String content, String charset) { |
|||
if (charset == null || "".equals(charset)) { |
|||
return content.getBytes(); |
|||
} |
|||
try { |
|||
return content.getBytes(charset); |
|||
} catch (UnsupportedEncodingException e) { |
|||
throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset); |
|||
} |
|||
} |
|||
|
|||
private static String createNonceStr(){ |
|||
return UUID.randomUUID().toString().replace("-",""); |
|||
} |
|||
|
|||
public static Map<String,Object> payToUsrWx( |
|||
String partner_trade_no,String openid,int total_fee,String desc,String spbill_create_ip) |
|||
throws Exception{ |
|||
final String SUCCESS = "SUCCESS"; |
|||
//1.构造请求字符串
|
|||
Map<String,Object> reqMap = new HashMap<>(); |
|||
reqMap.put("mch_appid",appid); |
|||
reqMap.put("mchid",mchid); |
|||
//reqMap.put("device_info",null); //不能添加空字段,Jackson会生成闭包xml <device_info/>导致签名失败
|
|||
reqMap.put("nonce_str",createNonceStr()); |
|||
reqMap.put("partner_trade_no",partner_trade_no); |
|||
reqMap.put("openid",openid); |
|||
reqMap.put("check_name","NO_CHECK"); |
|||
//reqMap.put("re_user_name",null);
|
|||
reqMap.put("amount",total_fee + ""); |
|||
reqMap.put("desc",desc); |
|||
reqMap.put("spbill_create_ip",spbill_create_ip); |
|||
reqMap.put("sign",createSign(reqMap)); |
|||
|
|||
//2.发送付款请求
|
|||
String reqrXml = JacksonUtil.mapToXml(reqMap,"xml"); |
|||
String respXml = HttpsUtil.httpsRequest(URL_PAY_TO_USR_WX,"POST",reqrXml, |
|||
WebConstant.PATH_WX_CRET,mchid); |
|||
System.out.println("---------------PayToUsrWx Request Xml-----------------"); |
|||
System.out.println(reqrXml); |
|||
System.out.println("---------------PayToUsrWx Response Xml-----------------"); |
|||
System.out.println(respXml); |
|||
System.out.println("---------------PayToUsrWx end-----------------"); |
|||
|
|||
//3.判断成功失败
|
|||
Map<String,Object> respMap = JacksonUtil.xmlToMap(new ByteArrayInputStream(respXml.getBytes())); |
|||
String return_code,result_code; |
|||
return_code = (String)respMap.get("return_code"); |
|||
result_code = (String)respMap.get("result_code"); |
|||
|
|||
if(!StrUtil.isEmpty(return_code) && !StrUtil.isEmpty(result_code)){ |
|||
if(return_code.equals(SUCCESS) && return_code.equals(result_code)){ |
|||
return respMap; |
|||
}else{ |
|||
throw new PayException("[" + respMap.get("err_code") + "]" |
|||
+ respMap.get("err_code_des")); |
|||
} |
|||
}else { |
|||
throw new PayException((String) respMap.get("return_msg")); |
|||
} |
|||
//return null;
|
|||
} |
|||
|
|||
public static Map<String,String> payToUsrBank(){ |
|||
//Fix Me
|
|||
return null; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue