实现思路
- 第一步:我们要先到微信开发者后台管理系统维护回调地址。主要作用:当用户对公众号做操作时,微信服务器会把相应的事件和参数回调至我们内部定制的接口,我们可以真对微信回调的时间和参数进行定制操作。详细可参考官网。
- 第二步:真对回调的项目地址,开发对应的服务。回调同一个地址分为GET和POST两个不同请求。
- GET请求处理,配置微信回调地址时的,对微信服务的请求认证。
- POST请求处理,认证成功后,后续微信服务器对于所有的事件和参数的回调都是POST请求
@RestController
public class WeChatController {
@Resource
private IWeChatService weChatService;
/**
* 微信公众号回调地址验证
*/
@ApiOperation(value = "微信公众号回调地址验证", httpMethod = "GET")
@GetMapping("/callback")
public String callback(String signature, String timestamp, String nonce, String echostr) {
log.info("signature:[{}],timestamp:[{}],nonce:[{}],echostr:[{}]", signature, timestamp, nonce, echostr);
// 验证请求是否来自微信服务器
if (!SignatureUtil.checkSignature(signature, timestamp, nonce, projectApp.getWxServerToken())) {
return null;
}
return echostr;
}
@ApiOperation(value = "公众号事件触发回调地址", httpMethod = "POST")
@PostMapping("/callback")
public String callback(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 处理微信服务器发来的消息
Map<String, String> map = XmlParserUtil.parseXml(request.getInputStream());
log.info("WeChatController callback res:[{}]", JSON.toJSONString(map));
//监听微信请求的事件类型,做出对应处理
String result = weChatService.handleEvent(projectApp, map);
//处理响应乱码问题
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
return result;
}
}
//Service层方法
@Transactional(rollbackFor = Exception.class)
@Override
public void handleEvent(ProjectApp projectApp, Map<String, String> map) {
//公众号消息类型
String msgType = map.get("MsgType");
//推送事件类型
String event = map.get("Event");
String openId = map.get("FromUserName");
//扫码关注,传入的参数
String eventKey = map.get("EventKey");
return "success";
}
public class XmlParserUtil {
/**
* xml字符串解析为Map
*
* @return Map
* @throws IOException
*/
public static Map<String, String> parseXml(InputStream is) {
//将输入流解析成Map
Map<String, String> map = new HashMap<>();
try {
//读取输入流获取文档对象
SAXReader reader = new SAXReader();
Document document = reader.read(is);
//根据文档对象获取根节点
Element root = document.getRootElement();
//获取所有的子节点
List<Element> elements = root.elements();
for (Element e : elements) {
map.put(e.getName(), e.getStringValue());
}
} catch (DocumentException e) {
e.printStackTrace();
}
return map;
}
/**
* 将 JavaBean 转换为 XML 字符串
*
* @param obj JavaBean 对象
* @return XML 字符串
*/
public static String beanToXml(Object obj) throws JAXBException {
JAXBContext context = JAXBContext.newInstance(obj.getClass());
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, false);
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
// 省略文档头
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
StringWriter writer = new StringWriter();
marshaller.marshal(obj, writer);
return writer.toString();
}
}
第一次GET请求认证通过后,返回微信服务器的请求入参echostr,微信服务器接收到后,代表认证成功。至此微信服务器对开发者服务器的认证操作完成。
- 第三步:当用户关注微信公众号时,微信服务器会回调开发者服务器,我们只需根据相应的事件和参数结合自身项目的业务取值适配即可。
目前最常用的两种触发关注公众号的操作是:
两者的区别是,扫码关注,可以带参数,通常会运用到关注公众号后会建立某种绑定关系的情况。根据自身的业务取衡量取决于那种方式即可。
- 获取微信公众号的openID
真对微信服务器回调我们的参数,进行处理,即可拿到流程流转的一些必要值域。例如OpenID,就是关注事件参数中的(FromUserName)。如果公众号和小程序已经在开发者平台绑定,可以通过这里的openID,去换取unionID 官网参考
/**
* 通过单个OpenID来获取用户基本信息(UnionID机制)
* @param openId 传入公众号openId
*/
public WeChatUserInfo getUserInfo(String openId, String accessToken) throws Exception {
if (StringUtils.isBlank(openId)) {
throw new Exception("传入openId不能为空");
}
String userInfoOpenidUri = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={0}&openid={1}&lang=zh_CN";
String url = MessageFormat.format(userInfoOpenidUri, accessToken, openId);
try {
JSONObject json = RequestUtil.get(url);
log.info("公众号用户openID:[{}]获取的unionID信息:[{}]", openId, json.toJSONString());
if (Objects.nonNull(json.get("errcode")) && Objects.isNull(json.get("data"))) {
return null;
}
WeChatUserInfo weChatUserInfo = JSON.to(WeChatUserInfo.class, json);
if (Objects.nonNull(weChatUserInfo)) {
return weChatUserInfo;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* @Author HaiJia.Chen
* @Description 获取用户的基本信息明细
* @Date 2023-04-10 15:13
**/
@Data
public class WeChatUserInfo {
private Integer subscribe;
private String openid;
private String language;
private String unionid;
private String remark;
private String groupid;
private String subscribe_scene;
}
获取到OpenID和UnionID后,根据项目的业务设定,去做相应的关系存储即可。处理完成后,会对微信服务器进行响应,然后微信服务器把最终结果响应给用户。
关注公众号推送小程序卡片
通常是在处理回调事件的结尾,去掉微信公众号官方的接口,去实现小程序卡片的推送 官网参考【发送小程序卡片(要求小程序与公众号已关联)】。代码如下:
/**
* 关注公众号后,推送小程序卡
*/
public boolean sendCardMsg(String accessToken, SendCardMsg cardMsg) {
if (StringUtils.isBlank(accessToken) || Objects.isNull(cardMsg)) {
return false;
}
String sendCardUri = configProperties.getWexin().getSendCardMessageUri();
String url = MessageFormat.format(sendCardUri, accessToken);
try {
JSONObject json = RequestUtil.post(url, cardMsg);
log.info("微信公众号推送小程序卡片返回:[{}]", json.toJSONString());
if (Objects.equals(0, json.get("errcode"))) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void sendCardMsg(String openId, String accessToken, String saleId) {
SendCardMsg sendCardMsg = new SendCardMsg();
sendCardMsg.setTouser(openId);
sendCardMsg.setMsgtype(WeChatEnum.SEND_CARD_MSGTYPE_MINIPROGRAMPAGE.getCode());
SendCardDetailMsg detailMsg = new SendCardDetailMsg();
detailMsg.setAppid("小程序APPID");
detailMsg.setTitle("卡片标题名称");
detailMsg.setPagepath("小程序跳转路径");
detailMsg.setThumb_media_id("图片ID");
sendCardMsg.setMiniprogrampage(detailMsg);
weChatUtil.sendCardMsg(accessToken, sendCardMsg);
}
如果我们选择了这样做,会带来一个问题,开发者管理后台无法设置的关注公众号自动推送一段欢迎语。
这时候假如我们既要推送卡片链接,又要推送一句欢迎语,该如何实现呢。答案是:使用被动回复功能 官网参考
被动回复:很简单也就是我们根据官方文档给微信请求我们的回调接口做返回值,即可实现欢迎语。
public String sendTextMsg(ProjectApp projectApp, String openId, String toUserName) {
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(openId);
textMessage.setFromUserName(toUserName);
textMessage.setCreateTime(System.currentTimeMillis());
textMessage.setMsgType(WeChatEnum.RESP_MESSAGE_TYPE_TEXT.getCode());
textMessage.setContent("欢迎关注小陈公众号~");
String result = "";
try {
result = XmlParserUtil.beanToXml(textMessage);
} catch (JAXBException e) {
e.printStackTrace();
}
return result;
}
生成带参数的公众号二维码
上面的开发流程中讲到了两种常用的扫码关注公众号的两种方式,那公众号二维码是如何生成的呢,又是如何带参数的呢?官网参考
/**
* 获取微信公众号带参数的二维码
*/
public String generateQrCode(String accessToken, WeChatQrCode weChatQrCode) {
// 1. 创建二维码ticket
String genQrcodeUri = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}";
String showQrCodeUri = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={0}";
String uri = MessageFormat.format(genQrcodeUri, accessToken);
try {
JSONObject json = RequestUtil.post(uri, weChatQrCode);
log.info("获取公众号二维码请求入参[{}]响应结果[{}]", json.toJSONString(), JSON.toJSONString(weChatQrCode));
if (Objects.nonNull(json.get("errcode")) && Objects.isNull(json.get("ticket"))) {
return null;
}
String qrCodeUrl = MessageFormat.format(showQrCodeUri, json.get("ticket"));
log.info("获取公众号二维码地址:[{}]", qrCodeUrl);
//获取上传位置
String path = "/xiao/chen";
FileUtil.createFolder(path);
String savePath = path + System.currentTimeMillis() + ".jpg";;
InputStream in = null;
FileOutputStream out = null;
long fileSizeInBytes = 0L;
try {
URL url = new URL(qrCodeUrl);
URLConnection conn = url.openConnection();
in = conn.getInputStream();
fileSizeInBytes = conn.getContentLengthLong();
byte[] buffer = new byte[1024];
int len;
out = new FileOutputStream(savePath);
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (Objects.nonNull(in)) {
in.close();
}
if (Objects.nonNull(out)) {
out.close();
}
}
//写入数据库,返回保存地址
//……
return previewUrl;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Test
public void genQRCode() {
String accessToken = weChatUtil.getAccessToken("test", 123L,"appId", "appSecret");
WeChatQrCode weChatQRCode = new WeChatQrCode();
weChatQRCode.setActionName("QR_LIMIT_SCENE");
Map<String, Object> test = new HashMap<>();
Map<String, Object> t = new HashMap<>();
t.put("scene_id", 3);
test.put("scene", t);
weChatQRCode.setActionInfo(test);
String Tes = weChatUtil.generateWxQrCode(accessToken, weChatQRCode);
System.out.println(Tes);
}
@Data
public class WeChatQrCode {
@ApiModelProperty(name = "expireSeconds", value = "二维码有效时间")
@JsonProperty("expire_seconds")
private int expireSeconds;
@ApiModelProperty(name = "actionName", value = "二维码类型")
@JsonProperty("action_name")
private String actionName;
@ApiModelProperty(name = "actionInfo", value = "二维码详细信息")
@JsonProperty("action_info")
private Object actionInfo;
}
微信公众号后台自定义菜单失效
开启服务器配置,允许微信服务器回调我们自己服务器的时候会导致失效。