当你关注公众号,然后在公众号里面发送消息,会收到回复,这个就是客服消息
参考文档:接收普通消息 接收事件推送 客服接口-发消息
想要对接客服消息,首先要获取access_token,这个可以参考我之前的文章:对接微信公众号-CSDN博客
回复消息的方式
回复消息的方式有两种,一种是接收到用户消息后直接在response里面回复,文档:回复文本消息 | 微信开放文档
这种方式比较简单,缺点是必须保证在5s内回复,否则会重试,不知道你有没有遇到过以下场景,当你发送一条消息,过了一会突然收到好几条相同的信息,就是卡顿重试造成的。
还有一种方式是客服消息,这种是后台主动给用户发消息, 功能更加强大,不仅仅可以回复用户消息,还可以在用户关注公众号,扫描二维码,点击菜单时回复用户消息,参考文档:客服接口-发送消息
由于客服消息是异步发送,在发送的过程中我们可以调用客服输入状态接口,实现以下效果
用户体验更好,参考文档: 客服输入状态
客服消息规则如下,就是说如果用户发送了消息,你可以在48小时内下发最多5条消息
场景 | 下发额度 | 额度有效期 |
---|---|---|
用户发送消息 | 5条 | 48小时 |
点击自定义菜单 | 3条 | 1分钟 |
关注公众号 | 3条 | 1分钟 |
扫描二维码 | 3条 | 1分钟 |
如何对接服务器,如何接收消息可以看我之前写的博客: 对接微信公众号-CSDN博客
Java代码实现
以下代码实现了客服回复文本消息,图文消息和图片消息
当用户输入文本则回复你好,输入图文则返回一篇新闻,输入图片则返回一张图片
@RestController
@RequestMapping("/wx")
@Api(tags = "微信管理")
public class WxController {@Autowiredprivate WeixinService weixinService;@RequestMapping("receiveWeixin")@ApiOperation(value = "接收微信消息")public void receiveWeixin(String signature, String timestamp, String nonce, String echostr,HttpServletRequest request, HttpServletResponse response) {try {//服务器校验String token = "123456";List<String> list = new ArrayList<>();list.add(token);list.add(timestamp);list.add(nonce);list = CollectionUtil.sort(list, (o1, o2) -> o1.compareTo(o2));String str = CollectionUtil.join(list, "");if (!signature.equals(SecureUtil.sha1(str))) {return;}if (StringUtils.isNotEmpty(echostr)) {//校验服务器response.getOutputStream().write(echostr.getBytes());response.getOutputStream().flush();return;}//接收消息Document document = XmlUtil.readXML(request.getInputStream());Map map = XmlUtil.xmlToMap(document);JSONObject json = JSONObject.parseObject(JSONObject.toJSON(map).toString()).getJSONObject("xml");System.out.println(json);//消息类型String msgType = json.getString("MsgType");//公众号微信账号String account = json.getString("ToUserName");if (msgType.equals("event")) {// 处理事件消息,比如当用户关注公众号可以下发个欢迎消息} else if (msgType.equals("text")) {// 处理用户消息WeixinReceiveText weixinReceiveText = JSONObject.parseObject(json.toJSONString(), WeixinReceiveText.class);ThreadUtil.execAsync(() -> weixinService.replyMessage(weixinReceiveText.getFromUserName(), weixinReceiveText.getContent()));}} catch (Exception e) {e.printStackTrace();}}@PostMapping(value = "/uploadMaterial")@ApiOperation(value = "上传资源")public JSONObject uploadMaterial(MultipartFile file) {return weixinService.addMaterial(file, "image");}
}
@Service
public class WeixinService {protected static Logger logger = LoggerFactory.getLogger(WeixinService.class);private final String TOKEN_KEY = "WEIXIN_TOKEN:";@Autowiredprivate RedisTemplate redisTemplate;private final static String appId = "你的appid";private final static String appsecret = "你的appsecret";public WxUser getUserInfoByCode(String code) {String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code";url = StrUtil.format(url, appId, appsecret, code);String res = HttpUtil.get(url);JSONObject tokenInfo = handleResult(res);String token = tokenInfo.getString("access_token");String openid = tokenInfo.getString("openid");String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}&lang=zh_CN";userInfoUrl = StrUtil.format(userInfoUrl, token, openid);res = HttpUtil.get(userInfoUrl);JSONObject userInfoJson = handleResult(res);return JSONObject.parseObject(userInfoJson.toJSONString(), WxUser.class);}public synchronized String getToken() {String redisKey = TOKEN_KEY + appId;String token = (String) redisTemplate.opsForValue().get(redisKey);if (StrUtil.isNotEmpty(token)) return token;JSONObject params = new JSONObject();params.put("grant_type", "client_credential");params.put("appid", appId);params.put("secret", appsecret);String str = HttpUtil.post(WeixinConstants.ACCESS_TOKEN_URL, params.toJSONString());JSONObject result = handleResult(str);token = result.getString("access_token");redisTemplate.opsForValue().set(redisKey, token, result.getIntValue("expires_in"), TimeUnit.SECONDS);return token;}private JSONObject handleResult(String str) {JSONObject result = JSONObject.parseObject(str);if (StrUtil.isNotBlank(result.getString("errcode"))) {if (result.getString("errcode").equals("0")) {return result;}if (result.getString("errcode").equals("40014")) {clearToken(appId);}throw new RuntimeException(StrUtil.format("微信接口出错, errcode: {}, msg: {}", result.getString("errcode"), result.getString("errmsg")));}return result;}public void clearToken(String appId) {String redisKey = TOKEN_KEY + appId;redisTemplate.delete(redisKey);}public JSONObject getMenuInfo() {String token = getToken();String str = HttpUtil.get(StrUtil.format(WeixinConstants.GET_MENU_INFO_URL, token));JSONObject result = handleResult(str);JSONObject menuInfo = result.getJSONObject("selfmenu_info");if (menuInfo == null) {menuInfo = new JSONObject();}return menuInfo;}public void createMenu(String menu) {String token = getToken();String str = HttpUtil.post(StrUtil.format(WeixinConstants.CREATE_MENU_URL, token), menu);handleResult(str);}public void customSend(String openId, String message) {String token = getToken();JSONObject body = new JSONObject();body.put("touser", openId);body.put("msgtype", "text");JSONObject content = new JSONObject();body.put("text", content);content.put("content", message);String str = HttpUtil.post(StrUtil.format(WeixinConstants.CUSTOM_SEND_URL, token), body.toJSONString());handleResult(str);}/*** 发送图片消息*/public void customSendPic(String openId, String mediaId) {String token = getToken();JSONObject body = new JSONObject();body.put("touser", openId);body.put("msgtype", "image");JSONObject image = new JSONObject();body.put("image", image);image.put("media_id", mediaId);String str = HttpUtil.post(StrUtil.format(WeixinConstants.CUSTOM_SEND_URL, token), body.toJSONString());handleResult(str);}public void customSendPicText(String openId, WeixinPicTextVO weixinPicTextVO) {String token = getToken();JSONObject body = new JSONObject();body.put("touser", openId);body.put("msgtype", "news");JSONObject news = new JSONObject();body.put("news", news);JSONArray articles = new JSONArray();news.put("articles", articles);articles.add(weixinPicTextVO);String str = HttpUtil.post(StrUtil.format(WeixinConstants.CUSTOM_SEND_URL, token), body.toJSONString());handleResult(str);}// type: image, voice, video, thumbpublic JSONObject addMaterial(MultipartFile file, String type) {File dir = null;try {String token = getToken();String url = StrUtil.format(WeixinConstants.ADD_METERIAL_URL, token, type);//获取临时文件夹目录String tempPath = System.getProperty("java.io.tmpdir");String dirName = UUID.randomUUID().toString();dir = new File(tempPath + dirName);dir.mkdirs();File tempFile = new File(dir.getAbsolutePath() + File.separator + file.getOriginalFilename());tempFile.deleteOnExit();file.transferTo(tempFile);HashMap<String, Object> paramMap = new HashMap<>();paramMap.put("media", tempFile);String str = HttpUtil.post(url, paramMap);return handleResult(str);} catch (Exception e) {throw new RuntimeException("上传文件出错", e);} finally {FileUtil.del(dir);}}// command: Typing,CancelTypingpublic void customTyping(String openId, String command) {String token = getToken();JSONObject body = new JSONObject();body.put("touser", openId);body.put("command", command);String str = HttpUtil.post(StrUtil.format(WeixinConstants.CUSTOM_TYPING_URL, token), body.toJSONString());handleResult(str);}public void replyMessage(String openId, String content) {try {customTyping(openId, "Typing");if ("文本".equals(content)) {customSend(openId, "你好");} else if ("图文".equals(content)) {WeixinPicTextVO weixinPicTextVO = new WeixinPicTextVO();weixinPicTextVO.setTitle("年轻人开始流行三无婚礼");weixinPicTextVO.setDescription("近年来,越来越多的年轻人告别繁杂冗余的俗套,简化仪式和环节,选择流程简单、时间充裕的极简婚礼");weixinPicTextVO.setUrl("https://www.jnnews.tv/guanzhu/p/2024-01/15/1027166.html");weixinPicTextVO.setPicurl("https://www.jnnews.tv/a/10001/202401/bdabe3e9925a31e867137ed95f722edc.jpeg");customSendPicText(openId, weixinPicTextVO);} else if ("图片".equals(content)) {// mediaId通过addMaterial获得customSendPic(openId, "im8rzmF--MD8vDCSnju1oif7lPzvgBMUepg0X1gh8CR6iD2L27CcxLhouIMigR2F");}} catch (Exception e) {logger.error("微信客服消息出错: {}", e);} finally {customTyping(openId, "CancelTyping");}}
}
public class WeixinConstants {/*** 获取token*/public static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token";/*** 获取菜单*/public static final String GET_MENU_INFO_URL = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token={}";/*** 创建菜单*/public static final String CREATE_MENU_URL = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token={}";/*** 客服接口-发消息*/public static final String CUSTOM_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={}";/*** 客服接口-输入状态*/public static final String CUSTOM_TYPING_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token={}";/*** 添加素材*/public static final String ADD_METERIAL_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token={}&type={}";
}
@Data
@ApiModel(value = "WeixinReceiveText", description = "WeixinReceiveText")
public class WeixinReceiveText extends WeixinBaseVO {private String Content;private String MsgId;private String MsgDataId;private String Idx;
}
@Data
public class WeixinPicTextVO {private String title;private String description;private String url;private String picurl;
}