1.使用微信公众号测试账号进行开发,申请测试公众号http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login
2.登录之后,能获取微信公众号的appID和appsecret
3.查看接口文档可以发现,每次请求接口都需要带access_token,所以首先需要查询token
4.查询token放入Redis(启动时查询一次token放入Redis,设置生效时间一小时,设置定时任务,每隔一小时刷新下token,也可以不存储token,每次请求接口都重新刷新下token)
@Scheduled(cron = "0 0/60 * * * ?")
public void execute() {
log.info("定时更新token,每60分钟跑一次,appId={},appSecret={}",appId,appSecret);
CtBossSender ctBossSender=new CtBossSender(appId,appSecret);
NutMap nutMap=ctBossSender.sendHttpGet();
log.info("获取到的Token为={}",JSON.toJSONString(nutMap));
try {
redisTemplate.opsForValue().set(MessageTypeEnum.ACCESS_TOKEN.getInfo(),nutMap.getString(MessageTypeEnum.ACCESS_TOKEN.getInfo()),60, TimeUnit.MINUTES);
}catch (Exception e){
log.error(e.getMessage(),e);
log.info("token更新异常....={}",JSON.toJSONString(nutMap));
return;
}
log.info("token更新成功....");
}
/**
* @Date 10:49 2020/7/21
* 项目启动时更新token信息 放入内存中
*/
@Component
@Slf4j
public class InitTokenUtil implements CommandLineRunner {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${appid}")
private String appId;
@Value("${appsecret}")
private String appSecret;
/*
* @Author
* @Description //项目启动时获取token,放入Redis
* @Date 15:06 2020/7/23
* @Param [args]
* @return
**/
@Override
public void run(String... args) throws Exception {
log.info("项目启动时更新token,appId={},appSecret={}",appId,appSecret);
CtBossSender ctBossSender=new CtBossSender(appId,appSecret);
NutMap nutMap=ctBossSender.sendHttpGet();
log.info("获取到的Token为={}",JSON.toJSONString(nutMap));
try {
redisTemplate.opsForValue().set(MessageTypeEnum.ACCESS_TOKEN.getInfo(),nutMap.getString(MessageTypeEnum.ACCESS_TOKEN.getInfo()),60, TimeUnit.MINUTES);
}catch (Exception e){
log.error(e.getMessage(),e);
log.info("token更新异常....={}",JSON.toJSONString(nutMap));
return;
}
log.info("token更新成功....");
}
}
5.查看文档获取创建、删除、获取菜单URL,文档里给出了URL和菜单示例
6.创建枚举类,放入URL
public enum ApiEum {
ACCESS_TOKEN_URL(" https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"),
BASE_URL(" https://api.weixin.qq.com/cgi-bin/"),
DELETE_PERSONAL_MENU_URL("menu/delconditional"),//删除个性化菜单
CREATE_PERSONALIZED_MENU_URL("menu/addconditional"),//创建个性化菜单
MENU_GET_URL("menu/get"),//自定义菜单的查询接口
MENU_DELETE_URL("menu/delete"),//自定义菜单删除
MENU_CREATE_URL("menu/create");//自定义菜单创建
private String info;
ApiEum(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
}
7.创建菜单请求实体
@Data
public class Button {
private String type;//菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型
private String name;//菜单标题,不超过16个字节,子菜单不超过40个字节
private String url;//网页链接,用户点击菜单可打开链接,不超过1024字节。当type为miniprogram时,不支持小程序的老版本客户端将打开本url
private String key;//菜单KEY值,用于消息接口推送,不超过128字节
private String appid;//小程序的appid
private String pagepath;//小程序的页面路径
private String media_id;//调用新增永久素材接口返回的合法media_id
private Button[] sub_button;//二级菜单数组,个数应为1~5个
}
@Data
public class Menu {
private Button[] button;//一级菜单数组,个数应为1~3个
}
8.请求微信平台创建菜单
/*
* @Author
* @Description //获取文档里的Menu示例,也可以根据格式一步一步的拼接
* @Date 14:40 2020/7/21
* @Param []
* @return {@link com.boot.wechart.entity.Menu}
**/
public Menu getSyStemMenu(){
String message=" {\n" +
" \"button\":[\n" +
" {\t\n" +
" \"type\":\"click\",\n" +
" \"name\":\"今日歌曲\",\n" +
" \"key\":\"V1001_TODAY_MUSIC\"\n" +
" },\n" +
" {\n" +
" \"name\":\"菜单\",\n" +
" \"sub_button\":[\n" +
" {\t\n" +
" \"type\":\"view\",\n" +
" \"name\":\"搜索\",\n" +
" \"url\":\"http://www.soso.com/\"\n" +
" },\n" +
" {\n" +
" \"type\":\"click\",\n" +
" \"name\":\"赞一下我们\",\n" +
" \"key\":\"V1001_GOOD\"\n" +
" }]\n" +
" }]\n" +
" }";
return JSON.parseObject(message, Menu.class);
}
@RequestMapping("/createMenu")
public String createMenu(){
Menu menu= MenuUtil.me().getSyStemMenu();//获取文档示例菜单
return menuService.creatMenu(menu);
}
/*
* @Author
* @Description //创建菜单
* @Date 13:44 2020/7/21
* @Param
* @return {@link null}
**/
@Override
public String creatMenu(Menu menu) {
//发起POST请求创建菜单
CtBossSender ctBossSender=new CtBossSender(ApiEum.MENU_CREATE_URL.getInfo()); //获取菜单结构
String accessToken=redisTemplate.opsForValue().get(MessageTypeEnum.ACCESS_TOKEN.getInfo());
log.info("创建菜单请求参数为={}",JSON.toJSONString(menu));
NutMap nutMap = ctBossSender.sendHttpPost(JSON.toJSONString(menu),accessToken);
log.info("创建菜单返回参数为={}",JSON.toJSONString(nutMap));
return JSON.toJSONString(nutMap);
}
9.获取、删除自定义菜菜单
@Override
public String deleteMenu() {
//发起GET请求删除菜单
CtBossSender ctBossSender=new CtBossSender(ApiEum.MENU_DELETE_URL.getInfo()); //获取菜单结构
String accessToken=redisTemplate.opsForValue().get(MessageTypeEnum.ACCESS_TOKEN.getInfo());
NutMap nutMap = ctBossSender.sendHttpGet(accessToken);
log.info("删除菜单返回参数为={}",JSON.toJSONString(nutMap));
return JSON.toJSONString(nutMap);
}
@Override
public String getMenu() {
CtBossSender ctBossSender=new CtBossSender(ApiEum.MENU_GET_URL.getInfo()); //获取菜单结构
String accessToken=redisTemplate.opsForValue().get(MessageTypeEnum.ACCESS_TOKEN.getInfo());
NutMap nutMap = ctBossSender.sendHttpGet(accessToken);
log.info("获取菜单返回参数为={}",JSON.toJSONString(nutMap));
return JSON.toJSONString(nutMap);
}
10.代码中用到的工具类
public enum MessageTypeEnum {
EVENT_TYPE_TEMPLATESENDJOBFINISH("TEMPLATESENDJOBFINISH"),//事件类型:TEMPLATESENDJOBFINISH(模板消息送达情况提醒)
ACCESS_TOKEN("access_token"),
EVENT_TYPE_VIEW("VIEW"),//事件类型:VIEW(自定义菜单URl视图)
EVENT_TYPE_CLICK("CLICK"),//事件类型:CLICK(点击菜单拉取消息)
EVENT_TYPE_LOCATION("location"),//事件类型:location(上报地理位置)
EVENT_TYPE_SCAN("EVENT_TYPE_SCAN"),//事件类型:scan(关注用户扫描带参二维码)
EVENT_TYPE_UNSUBSCRIBE("unsubscribe"),//事件类型:取消订阅
EVENT_TYPE_SUBSCRIBE("subscribe"),//事件类型:订阅
RESP_MESSAGE_TYPE_TEXT ("text"),//返回消息类型:文本
RESP_MESSAGE_TYPE_IMAGE("image"),//消息返回类型:图片
RESP_MESSAGE_TYPE_VOICE ("voice"),//消息返回类型:语音
RESP_MESSAGE_TYPE_MUSIC ("music"),//消息返回类型:音乐
RESP_MESSAGE_TYPE_NEWS ("news"),//消息返回类型:图文
RESP_MESSAGE_TYPE_VIDEO("video"),//消息返回类型:视频
REQ_MESSAGE_TYPE_SHORTVIDEO("shortvideo"),//请求消息类型:小视频
REQ_MESSAGE_TYPE_EVENT("event"),//请求消息类型:事件推送
REQ_MESSAGE_TYPE_LINK("link"),//请求消息:类型链接
REQ_MESSAGE_TYPE_LOCATION("地理位置"),//请求消息类型:地理位置
REQ_MESSAGE_TYPE_VIDEO("vedio"),//请求消息类型:视频
REQ_MESSAGE_TYPE_VOICE("voice"),//请求消息类型:语音
REQ_MESSAGE_TYPE_IMAGE("image"),//请求消息类型:图片
REQ_MESSAGE_TYPE_TEXT("text");//请求消息类型:文本
private String info;
MessageTypeEnum(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
}
public class CtBossSender {
private String url;
private OkHttpClient httpRequestClient;
/*
* @Author
* @Description //token url初始化
* @Date 10:29 2020/7/21
* @Param [appid, secret]
* @return {@link null}
**/
public CtBossSender(String appid, String secret) {
StringBuilder urls=new StringBuilder(ApiEum.ACCESS_TOKEN_URL.getInfo()).append("&appid=").append(appid).append("&secret=").append(secret);
this.url =urls.toString() ;
httpRequestClient = OKHttp3Utils3.getHttpClient(60);
}
/*
* @Author
* @Description //请求微信公众号接口方法的初始化
* @Date 10:31 2020/7/21
* @Param [function]
* @return {@link null}
**/
public CtBossSender(String function) {
StringBuilder urls=new StringBuilder(ApiEum.BASE_URL.getInfo()).append(function);
this.url = urls.toString();
httpRequestClient = new OkHttpClient();
}
public CtBossSender() {
httpRequestClient = new OkHttpClient();
}
/*
* @Author
* @Description //url+token
* @Date 10:32 2020/7/21
* @Param [token]
* @return
**/
public void init(String token) {
StringBuilder reqUrl = new StringBuilder(this.url).append("?");
reqUrl.append("&access_token=").append(token);
this.url = reqUrl.toString();
}
/**
* 发送GET请求获取token
*
* @return
* @throws IOException
*/
public NutMap sendHttpGet(){
log.info("发送请求的url:url={}",url);
Response response = null;
NutMap responseBody =null;
try {
response = OKHttp3Utils3.get(url);
responseBody= JSON.parseObject(response.body().string(),NutMap.class);
} catch (IOException e) {
log.error(e.getMessage(),e);
}
log.info("Send iot request end... responseBody={}", JSON.toJSONString(responseBody));
return responseBody;
}
/**
* 发送GET请求获取数据
*
* @return
* @throws IOException
*/
public NutMap sendHttpGet(String token){
init(token);
log.info("发送请求的url:url={}",url);
Response response = null;
NutMap responseBody =null;
try {
response = OKHttp3Utils3.get(url);
responseBody= JSON.parseObject(response.body().string(),NutMap.class);
} catch (IOException e) {
log.error(e.getMessage(),e);
}
log.info("Send iot request end... responseBody={}", JSON.toJSONString(responseBody));
return responseBody;
}
/**
* 发送POST请求获取数据
*
* @return
* @throws IOException
*/
public NutMap sendHttpPost(String requestBody,String token){
init(token);
log.info("发送请求的url:url={}",url);
NutMap response = JSON.parseObject(OKHttp3Utils3.post(url,requestBody),NutMap.class);
log.info("Send iot request end... responseBody={}", JSON.toJSONString(response));
return response;
}
}
public final class OKHttp3Utils3 {
public static int DEFAULT_TIME_OUT = 10;
/**
* 全局实例可以保持http1.1 连接复用,线程池复用, 减少tcp的网络连接,关闭,
* 如果每次一个请求,在高并发下,thread增多到1W,close_wait持续增加到6k。
*/
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
.connectTimeout(10, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).writeTimeout(10, TimeUnit.SECONDS).build();
private static final MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
private static final MediaType FORM_TYPE = MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8");
/**
* 不同timeout的连接池
*/
public static ConcurrentHashMap<Integer, OkHttpClient> cacheClients = new ConcurrentHashMap();
public static OkHttpClient getHttpClient(int timeout) {
if (timeout == 0 || DEFAULT_TIME_OUT == timeout) {
return OK_HTTP_CLIENT;
} else {
OkHttpClient okHttpClient = cacheClients.get(timeout);
if (okHttpClient == null) {
return syncCreateClient(timeout);
}
return okHttpClient;
}
}
private static synchronized OkHttpClient syncCreateClient(int timeout) {
OkHttpClient okHttpClient;
okHttpClient = cacheClients.get(timeout);
if (okHttpClient != null) {
return okHttpClient;
}
okHttpClient = new OkHttpClient.Builder().connectTimeout(timeout, TimeUnit.SECONDS).readTimeout(timeout, TimeUnit.SECONDS).writeTimeout(timeout, TimeUnit.SECONDS).build();
cacheClients.put(timeout, okHttpClient);
return okHttpClient;
}
/**
* GET请求
*
* @param url
* @return Optional<String>
*/
public static Response get(String url, int timeout) throws IOException {
Request request = new Request.Builder().url(url)
.build();
return getHttpClient(timeout).newCall(request).execute();
}
public static Response get(String url) throws IOException {
return get(url, 60);
}
/**
* POST请求,参数为json格式。
*
* @param url
* @param json
* @return Optional<String>
*/
public static String post(String url, String json, int timeout) throws Exception {
long start = System.currentTimeMillis();
try {
RequestBody body = RequestBody.create(mediaType, json);
Request request = new Request.Builder().url(url).post(body).build();
return getHttpClient(timeout).newCall(request).execute().body().string();
} catch (Exception e) {
throw e;
} finally {
log.info("request url {} ,total time {} ms", url, (System.currentTimeMillis() - start));
}
}
public static String post(String url, String json) {
try {
return post(url, json, 60);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static String postByFormType(String url, String form) throws Exception {
long start = System.currentTimeMillis();
try {
RequestBody body = RequestBody.create(FORM_TYPE, form);
Request request = new Request.Builder().url(url).post(body).build();
return getHttpClient(0).newCall(request).execute().body().string();
} catch (Exception e) {
throw e;
} finally {
log.info("request url {} ,total time {} ms", url, (System.currentTimeMillis() - start));
}
}
/**
* 根据不同的类型和requestbody类型来接续参数
*
* @param url
* @param mediaType
* @param inputStream
* @return
* @throws Exception
*/
public static String post(String url, MediaType mediaType, InputStream inputStream) throws Exception {
RequestBody body = createRequestBody(mediaType, inputStream);
Request request = new Request.Builder().url(url).post(body).build();
return OK_HTTP_CLIENT.newCall(request).execute().body().string();
}
private static RequestBody createRequestBody(final MediaType mediaType, final InputStream inputStream) {
return new RequestBody() {
// @Nullable
@Override
public MediaType contentType() {
return mediaType;
}
@Override
public long contentLength() throws IOException {
try {
return inputStream.available();
} catch (IOException e) {
return 0;
}
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(inputStream);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
};
}
}
ps.代码中实体对象没有get.set方法,是因为引入了lombok插件,附源码地址https://gitee.com/bjiangAnhui/yichengyoushaonian/tree/master/boot-wechart