背景
由于公司多个项目都是微信公众号开发的项目,以前对公众号这块儿懵懵懂懂的,于是花了两天的时间研究了一下微信公众号开发的流程特此记录下来。
要实现的功能
- 创建菜单
- 授权登录
- 消息接收与回复
- 模板消息发送
项目技术
- 后端SpringBoot使用ruoyi框架+wx-java(对微信API的封装方便调用)
- 前端Vue3使用vue3-vant-mobile框架
准备工作
准备域名
我们可以通过ngrok获取一个域名。
- 访问网址 www.ngrok.cc/ 注册一个账号
- 注册登录后点击实名认证去认证,需要支付2元。

- 点击隧道管理的开通隧道拉到最底部有个免费的服务器点击购买

- 域名配置如图最后点击确定

- 下载ngrok的客户端www.ngrok.cc/download.ht...
- 启动ngrok双击Sunny-Ngrok 启动工具.bat并将后台的隧道id粘贴到控制台并回车

申请公众测试号
- 申请地址mp.weixin.qq.com/debug/cgi-b...

- 配置js安全域名(就是你申请的域名,js安全域名配置为必须微信所有接口都依赖这个接口)

准备nginx
- 安装nginx这里不做介绍可以自行百度安装步骤
- cmd切换到nginx安装的bin目录
- 启动命令 start nginx.exe
- 停止命令 nginx.exe -s stop
- 重新加载配置文件命令 nginx.exe -s reload
- 这时候通过分配给你的域名访问应该会返回nginx的默认页面,因为nginx默认就是80端口。如果看到如下页面说明域名映射访问成功 。

功能实现
创建菜单
- pom.xml依赖wx-java

- 在application.yml配置测试号的appid和secret

- 启动后端若依项目(启动流程可以登录若依官网查看)

- 编写创建菜单的接口
java
@RestController
@RequestMapping("/oauth")
public class OauthController extends BaseController {
@Autowired
private WxMpService wxMpService;
@GetMapping("/menu")
public R<String> createMenu() {
String menuJsonStr = "{\n" +
" "button": [\n" +
" {\n" +
" "type": "view",\n" +
" "name": "知识干货",\n" +
" "url": "http://gzh.free.idcfengye.com"\n" +
" }\n" +
" ]\n" +
"}";
try {
wxMpService.getMenuService().menuCreate(menuJsonStr);
return R.ok("菜单创建成功");
} catch (WxErrorException e) {
logger.error("菜单创建失败:{}",e.getMessage());
}
return R.ok("菜单创建失败");
}
}
- 放行接口

- 修改nginx配置并重启
由于域名映射到了nginx的80端口,我们后端启动的端口是8080所以我们需要让ngixn代理并转发到我们的后端项目中 
- 测试验证
通过访问 域名/backend/oauth/menu 可以看到我们测试号的菜单创建成功了。

授权登录并获取用户信息

- 前端代码改造
这里我简单改了一下就判断本地是否存储openId有的话可以放行,没有的话调用接口获取openId也就是获取授权。
2. 打包部署到nginx中
- 将打包好的dist包重命名为gzh并部署到nginx中html
- 修改nginx.conf配置并重启

- 测试号配置上用户授权的域名与js安全域名地址保持一致

4. 后端接口
- /index接口为构建授权链接:参数一为微信授权成功后回调的我们的地址也就是我们的result接口在这个接口中可以通过request获取微信的code,通过code可以获取accesstoken,通过token可以获取用户信息和openid。第二个参数为授权的方式总共两种一种USERINFO这种可以拿到用户的头像、昵称、openId但是可以回需要用户点击授权的确认按钮,第二种是BASE只可以拿到openId不需要用户同意俗称静默授权。第三个参数是url是上面前端调用我们接口传过来的,意思是授权成功后要跳转到前端的哪个页面。
js
@RestController
@RequestMapping("/oauth")
public class OauthController extends BaseController {
@Autowired
private WxMpService wxMpService;
@GetMapping("/index")
public void auth(@RequestParam String url,HttpServletResponse response) {
try {
String authUrl = wxMpService.getOAuth2Service().buildAuthorizationUrl(
"http://xxxx.com/backend/oauth/result",
WxConsts.OAuth2Scope.SNSAPI_USERINFO, url);
logger.info("构建的授权链接:" + authUrl);
response.sendRedirect(authUrl);
} catch (IOException e) {
logger.error("构建授权url异常:{}",e.getMessage());
}
}
@GetMapping("/result")
public void authResult(HttpServletRequest request, HttpServletResponse response) throws WxErrorException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
parameterMap.forEach((k, v) -> {
logger.info("微信授权回调的返回的参数:{}----{}",k,v);
});
// 根据code换取用户唯一标识openId
String code = request.getParameter("code");
// 传递过来要重定向到的前端页面
String state = request.getParameter("state");
// 获取token
WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
logger.info("token:{}",accessToken);
// 获取用户基本信息
WxOAuth2UserInfo userInfo = wxMpService.getOAuth2Service().getUserInfo(accessToken,null);
logger.info("用户信息:{}",userInfo);
response.sendRedirect("http://gzh.free.idcfengye.com?openId="+userInfo.getOpenid());
}
}
- 测试
我们通过点击公众号的的菜单可以看到授权信息已经拿到了


消息接收和自动回复
- 编写消息验证的接口
java
package com.qmc.web.controller.message;
@RestController
@RequestMapping("/message")
public class MessageController extends BaseController {
@Autowired
private WxMpService wxMpService;
@Autowired
private WxMpMessageRouter wxMpMessageRouter;
/**
* 接通微信消息,微信会发送验证到这个接口,接口要回复验证通过的回执信息。不然测试号的消息域名地址配置会提示配置失败
* */
@GetMapping("/index")
public void messageYz(HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.info("进来了");
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
String signature = request.getParameter("signature");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
logger.info("参数:{},{},{}",signature,nonce,timestamp);
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
// 消息签名不正确,说明不是公众平台发过来的消息
response.getWriter().println("非法请求");
return;
}
String echostr = request.getParameter("echostr");
if (StringUtils.isNotBlank(echostr)) {
// 说明是一个仅仅用来验证的请求,回显echostr
response.getWriter().println(echostr);
return;
}
String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ?
"raw" :
request.getParameter("encrypt_type");
if ("raw".equals(encryptType)) {
// 明文传输的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(request.getInputStream());
wxMpMessageRouter = new WxMpMessageRouter(wxMpService);
WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
if (outMessage == null) {
//为null,说明路由配置有问题,需要注意
response.getWriter().write("");
}
response.getWriter().write(outMessage.toXml());
}
}
- 测试号配置消息域名和token(这个token随便写),点击保存会发送请求到你配置的这个url中只有正确响应才能配置成功

- application.yml中配置token

- 编写回复消息
- 编写接口,微信所有监听到的用户事件都会回调到这个接口中,注意:测试号配置的消息url、验证消息的接口以及接收消息的接口地址要一致,验证的是get请求,接收消息的是post请求。
java
package com.qmc.web.controller.message;
@RestController
@RequestMapping("/message")
public class MessageController extends BaseController {
@Autowired
private WxMpService wxMpService;
@Autowired
private WxMpMessageRouter wxMpMessageRouter;
/**
* 接通微信消息,微信会发送验证到这个接口,接口要回复验证通过的回执信息。不然测试号的消息域名地址配置会提示配置失败
* */
@GetMapping("/index")
public void messageYz(HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.info("进来了");
response.setContentType("text/html;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
String signature = request.getParameter("signature");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
logger.info("参数:{},{},{}",signature,nonce,timestamp);
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
// 消息签名不正确,说明不是公众平台发过来的消息
response.getWriter().println("非法请求");
return;
}
String echostr = request.getParameter("echostr");
if (StringUtils.isNotBlank(echostr)) {
// 说明是一个仅仅用来验证的请求,回显echostr
response.getWriter().println(echostr);
return;
}
String encryptType = StringUtils.isBlank(request.getParameter("encrypt_type")) ?
"raw" :
request.getParameter("encrypt_type");
if ("raw".equals(encryptType)) {
// 明文传输的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(request.getInputStream());
wxMpMessageRouter = new WxMpMessageRouter(wxMpService);
WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(inMessage);
if (outMessage == null) {
//为null,说明路由配置有问题,需要注意
response.getWriter().write("");
}
response.getWriter().write(outMessage.toXml());
}
}
/**
* 接收微信事件如:用户关注、用户发送消息给公众号并回复消息给用户
* */
@PostMapping("/index")
public String receiveMsg(HttpServletRequest request) throws IOException {
String signature = request.getParameter("signature");
String nonce = request.getParameter("nonce");
String timestamp = request.getParameter("timestamp");
logger.info("参数校验:{},{},{}",signature,nonce,timestamp);
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能为伪造的请求!");
}
ServletInputStream inputStream = request.getInputStream();
WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(inputStream);
logger.info(wxMpXmlMessage.toString());
WxMpXmlOutMessage outMessage = wxMpMessageRouter.route(wxMpXmlMessage);
if (outMessage == null) {
return "";
}
logger.info("组装回复的消息:{}",outMessage.toXml());
return outMessage.toXml();
}
}
- 编写路由这里的意思是将微信下发的所有消息传输到handler,具体更多配置详见wx-java
js
package com.qmc.web.config;
import com.qmc.web.handler.MessageHandler;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WxMpConfiguration {
@Autowired
private final MessageHandler messageHandler;
public WxMpConfiguration(MessageHandler messageHandler) {
this.messageHandler = messageHandler;
}
@Bean
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
// 所有消息都交给同一个 handler 处理
router.rule().async(false).handler(messageHandler).end();
return router;
}
}
- 消息具体处理的handler,这里有各种消息关注、文本、图片等这里我们只测试文本消息,用户输入java关键字我们回复一个你好呀。
java
package com.qmc.web.handler;
@Component
public class MessageHandler implements WxMpMessageHandler {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) {
String content = wxMessage.getContent(); // 用户发送的内容
String event = wxMessage.getEvent(); // 事件类型(如 subscribe)
logger.info("接收到消息:{},{}",content,event);
// 1. 处理关注事件
if ("subscribe".equals(event)) {
return WxMpXmlOutMessage.TEXT()
.content("欢迎关注!😊")
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
// 2. 处理文本消息
if (wxMessage.getMsgType().equals("text") && content != null) {
if ("java".equalsIgnoreCase(content.trim())) {
return WxMpXmlOutMessage.TEXT()
.content("你好呀!")
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
}
// 3. 默认回复(可选)
return WxMpXmlOutMessage.TEXT()
.content("感谢您的消息,但我不懂呢~")
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
}
- 测试,发送java我们收到了你好呀!的回复。

模板消息
- 测试号配置模板消息

- 编写发送模板的接口,这里我们直接通过接口发送,主要需要模板id和接收人的openId。这两个都有的一个是测试号刚配置模板消息后生成的id,一个是用户之前授权后拿到的openId。
java
package com.qmc.web.controller.message;
@RestController
@RequestMapping("/message")
public class MessageController extends BaseController {
@Autowired
private WxMpService wxMpService;
@Autowired
private WxMpMessageRouter wxMpMessageRouter;
/**
* 发送模板消息
* */
@GetMapping("/template")
public R<String> sendTemplate() {
WxMpTemplateMessage wxMpTemplateMessage = WxMpTemplateMessage.builder()
.toUser("oqK7e7bvZZAYx7JAztSuILhLTuFA")
.templateId("PfQbB7sS2pL6gAL8MNlaMkLxqEhw-Zj12jKzqRCZrLY")
.url("https://www.baidu.com")
.build();
wxMpTemplateMessage.addData(new WxMpTemplateData("first",
"您好,您提请的材料不齐全或不准确,需补充完善,请到"我的中心"重新提交相关材料。","#FF00FF"));
wxMpTemplateMessage.addData(new WxMpTemplateData("keyword1",
"2014年05月02日","#FF00FF"));
wxMpTemplateMessage.addData(new WxMpTemplateData("keyword2",
"姓名变更","#FF00FF"));
wxMpTemplateMessage.addData(new WxMpTemplateData("keyword3",
"待受理","#FF00FF"));
wxMpTemplateMessage.addData(new WxMpTemplateData("remark",
"我们将在三个工作日内给你反馈,反馈的形式将通过微信公众号发送消息给您,请注意查收!","#FF00FF"));
try {
String responseContent = wxMpService.post(MESSAGE_TEMPLATE_SEND, wxMpTemplateMessage.toJson());
final JsonObject jsonObject = GsonParser.parse(responseContent);
return R.ok(jsonObject.toString());
} catch (WxErrorException e) {
return R.fail(e.getError().getErrorCode(), e.getError().getErrorMsg());
}
}
}
- 测试,可以看到模板消息正常发送了。

结语
以上就是前后端分离微信公众号项目开发的流程和功能实现,其中获取的用户信息可以存入数据库中做更多的事情。openId是对此公众号的唯一标识,后期实现的诸多功能都需要用到。