基于Spring Boot微信小程序服务端开发。
小程序客服开发过程中,往往需要在用户进入小程序客服时发送相关引导信息以吸引用户,防止潜在用户流失。根据微信官方的开发文档,可以发现微信会推送用户进入小程序客服的事件到服务端。那么只需要在推送时在服务端调用微信提供的发送信息的接口就能完成这一需求。然后就愉快地开始了编码。
public String miniCsMessage(
@ApiParam(value = "签名", required = false) @RequestParam(value = "signature", required = false) String signature,
@ApiParam(value = "时间戳", required = false) @RequestParam(value = "timestamp", required = false) String timestamp,
@ApiParam(value = "验证参数", required = false) @RequestParam(value = "nonce", required = false) String nonce,
@ApiParam(value = "openId", required = false) @RequestParam(value = "openid", required = false) String openId,
@ApiParam(value = "回显验证参数", required = false) @RequestParam(value = "echostr", required = false) String echostr,
@ApiIgnore HttpServletRequest request) throws Exception {
if (StringUtils.isNoneEmpty(echostr)) {
return echostr;
}
// 验证微信签名
if (!WeChatUtils.checkSignature(signature, nonce, timestamp)) {
return echostr;
}
WeChatMiniCSNotifyMessage weChatMiniCSNotifyMessage = WeChatUtils.getWeChatMiniCSNotifyMessage(openId, request.getInputStream());
weChatService.handleWeChatMiniCSNotifyEvent(weChatMiniCSNotifyMessage);
return echostr;
}
这里采用了发布者-订阅者模式,将微信推送事件封装成WeChatMiniCSNotifyEvent然后交给订阅者处理。在订阅者中对具体事件进行处理,部分代码如下。
在这里插入代码片public void onApplicationEvent(WeChatMiniCSEvent weChatMiniCSEvent) {
WeChatMiniCSNotifyMessage weChatMiniCSNotifyMessage = weChatMiniCSEvent.getWeChatMiniCSNotifyMessage();
.............
//用户点开客服等其他事件
if ("event".equals(weChatMiniCSNotifyMessage.getType())) {
//用户点开小程序客服事件
if("user_enter_tempsession".equals(weChatMiniCSNotifyMessage.getEvent())) {
.........
sendWelcomeMsgToUser(openId);
.........
} else{
log.info(String.format("用户openId:%s ,已发送小程序客服消息",openId));
}
}
}
其中方法sendWelcomeMsgToUser(openId)中封装了调用微信api给用户发送文字消息和图片的欢迎消息。一切看似没有任何问题,并且在自己测试时由于同时测试了用户发消息给客服后再次进入客服界面的事件。(巨坑)所以进入时能成够发送消息。可是后来线上环境测试时,所有用户初次进入时都不能推送成功。而微信api返回的错误消息是{40003:invalid openid}。有点无语。。。。后来在社区咨询了一下才发现微信在4月9号后就不支持用户进入小程序客服界面时推送消息了,而由微信统一发送****为您服务。那么自测时为什么会成功呢?因为自测时主动给小程序客服发送了消息,微信规定收到用户消息后可以推送不超过五条下行消息!!!
这个坑的根本原因应该是自己看开发文档没看仔细。背锅+1。由于小程序客服需要推送图片消息给用户,所以需要提前上传图片然后获取mediaId,之后通过将制定图片发送给用户。而素材的有效期是三天,所以在我上传图片三天后,突然就不能推送图片消息了,而文字消息却可以成功推送。图片消息推送返回的结果仍然是{40003:invalid openid}。可以说是非常之不友好了。。。百思不得其解只能去社区求助官方,官方的回复还是挺快的。明确这个素材是临时的后,貌似最直接的方案就是用定时任务来在缓存中维护一份最新的mediaId了。首先设定定时任务每天凌晨三点进行图片上传。
public class UploadImageTask {
@Autowired
private WechatMiniCSEventLisener wechatMiniCSEventLisener;
//每天凌晨三点上传小程序图片素材
@Scheduled(cron = "0 0 3 * * ?")
private void uploadTask(){
log.info("执行微信小程序客服图片素材上传任务");
wechatMiniCSEventLisener.processUploadTask();
}
}
然后编写具体的任务,这里读取图片文件时本地执行是可以直接读取到你resource文件下的图片的,但是打包成jar包之后是不能直接读的。踩坑之路漫漫。
/**
* 上传所有微信客服消息图片
*/
public Map uploadWechatTeacherImages(){
Map result = new HashMap<>();
try {
for (int i = 0; i < WECHAT_LEDU_MINI_TEACHER_NUMBER; i++) {
String fileName = String.valueOf(i)+"_dic.jpg";
String filePath = "/images/teacherImage/" +fileName;
InputStream inputStream =this.getClass().getResourceAsStream(filePath);
File file = new File(fileName);
FileUtils.writeByteArrayToFile(file, toByteArray(inputStream));
WeChatTeacherImageDTO weChatTeacherImageDTO = uploadOneImage(file);
if(weChatTeacherImageDTO!=null) {
weChatTeacherImageDTO.setId(String.valueOf(i));
result.put(String.valueOf(i), weChatTeacherImageDTO);
}else{
log.info("上传单张图片失败,fileName:{},filePath:{}",fileName,filePath);
}
}
return result;
}catch (Exception e){
log.error("上传微信客服消息图片失败",e);
return new HashMap<>();
}
}
到这里感觉大坑已经踩得差不多了,信心满满地写完了定时任务的实现。一测。。。上传代码以及bug如下0.0
public T wechatUploadFile(String url,File file,Class responseType){
if(!file.exists()){
return null;
}
MultiValueMap param = new LinkedMultiValueMap<>();
FileSystemResource fileResource = new FileSystemResource(file);
param.add("media", fileResource);
HttpHeaders headers = new HttpHeaders();
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
headers.setContentType(MediaType.parseMediaType("multipart/form-data; charset=UTF-8"));
HttpEntity> requestEntity = new HttpEntity<>(param,headers);
Map uriVariables = new HashMap<>();
uriVariables.put("access_token",weChatComponent.getLeduAccessToken());
return restTemplate.postForEntity(url, requestEntity, responseType,uriVariables).getBody();
}
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/plain]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:121)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:994)
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:977)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:737)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:691)
at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:454)
at com.example.dewey.Controller.BaseController.uploadOneImage(BaseController.java:128)
at com.example.dewey.Controller.BaseController.uploadWechatTeacherImages(BaseController.java:94)
at com.example.dewey.Controller.BaseController.test(BaseController.java:78)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:897)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
大致意思就是RestTemplate没法解析text/plain这种type的返回。又是没遇到过的问题,只能问度娘了。解决方法也还算简单,不能解析就给RestTemplate添加这种解析转换器。具体操作如下
首先定义转换器并让他支持text/plain
@Component
@Configuration
public class WxMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public WxMappingJackson2HttpMessageConverter(){
List mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.TEXT_PLAIN);
setSupportedMediaTypes(mediaTypes);
}
}
然后在RestTemplate中追加该解析器
@Configuration
public class RestTemplateConfiguration {
@Autowired
private WeChatHttpMessageConverter weChatHttpMessageConverter;
@Autowired
private WxMappingJackson2HttpMessageConverter wxMappingJackson2HttpMessageConverter;
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List> messageConverters = restTemplate.getMessageConverters();
messageConverters.add(weChatHttpMessageConverter);
messageConverters.add(wxMappingJackson2HttpMessageConverter);
return restTemplate;
}
}
真是一波三折。
AppID和AppSecret
由于之前公众号存在一套AppID和AppSecret,然后小程序和公众号的获取AccessToken的接口是一样的。前面一直用着公众号的AppID和AppSecret获取AccessToken来给小程序发送消息,也是排查了很久。
AccessToken
AccessToken的获取接口每次都是返回新的AccessToken并会将之前的AccessToken置为失效。所以有多台机器的情况下得将AccessToken放入缓存共用,而且在测试环境不能进行调用,不然会导致线上环境AccessToken不可用,导致整个服务不可用。
解析请求流
Java流数据只能读取一次,只能copy,流下了没技术的泪水。正确打开方式
//request.getInputStream()
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(),outputStream);
InputStream inputStream1 = new ByteArrayInputStream(outputStream.toByteArray());
InputStream inputStream2 = new ByteArrayInputStream(outputStream.toByteArray());