该部分内容包括:

  • 公众号的申请、登陆以及绑定用户
  • 公众号菜单的推送和删除
  • 内网穿透工具的使用

公众号申请

打开微信公众平台,登陆:

菜单推送

实现效果

工具依赖

使用工具完成菜单推送、消息推送等功能。

<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>

小程序配置

拷贝微信公众平台的 appID 和 appsecret

application-dev.yaml
wx:
mp:
app-id: wxxxxxxxxxxxxxxxxx
secret: xxxxxxxxxxxxxxxxxxxxxx

URL设置

公众号菜单的类型一共有三种:

  • view:表示网页类型
  • click:表示点击类型
  • miniprogram:表示小程序类型

我们需要设置网页类型的跳转URL,可以在配置文件中固定

application-dev.yaml
wechat:
prefix: http://vvvckt.natappfree.cc/oa/#

代码实现

MenuServiceImpl.java
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {

private final WxMpService wxMpService;

@Value("${wechat.prefix}")
private String prefix;

public MenuServiceImpl(WxMpService wxMpService) {
this.wxMpService = wxMpService;
}

/**
* 获取所有菜单
*/
@Override
public List<Menu> listMenu() {
List<Menu> menuList = baseMapper.selectList(null);
List<Menu> parents = menuList.stream()
.filter(menu -> menu.getParentId() == 0)
.collect(Collectors.toList());
ArrayList<Menu> menus = new ArrayList<>();
for (Menu menu : parents) {
List<Menu> children = menuList.stream()
.filter(item -> Objects.equals(item.getParentId(), menu.getId()))
.collect(Collectors.toList());
menu.setChildren(children);
menus.add(menu);
}
return menus;
}

/**
* 同步菜单到微信公众号
*/
@Override
public void syncMenu() throws WxErrorException {
// 查询菜单数据,封装成微信官方格式
List<Menu> menuList = this.listMenu();
// 菜单
JSONArray buttonList = new JSONArray();
for (Menu menu : menuList) {
JSONObject button = new JSONObject();
button.put("name", menu.getName());
if (CollectionUtils.isEmpty(menu.getChildren())) {
button.put("type", menu.getType());
button.put("url", prefix + menu.getUrl());
} else {
JSONArray subButton = new JSONArray();
for (Menu child : menu.getChildren()) {
JSONObject view = new JSONObject();
String type = child.getType();
view.put("type", type);
view.put("name", child.getName());
if (type.equals("click")) {
// 点击类型
view.put("key", child.getMeunKey());
} else {
// 网页或者小程序类型
view.put("url", prefix + child.getUrl());
}
subButton.add(view);
}
button.put("sub_button", subButton);
}
buttonList.add(button);
}
// 菜单对象
JSONObject button = new JSONObject();
button.put("button", buttonList);

// 推送
wxMpService.getMenuService().menuCreate(button.toString());
}

/**
* 删除微信公众号菜单
*/
@Override
public void removeMenu() throws WxErrorException {
wxMpService.getMenuService().menuDelete();
}
}

MenuServiceImpl 包括查询菜单,同步菜单,删除菜单功能。

官方对自定义菜单格式要求如下:

{
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"miniprogram",
"name":"wxa",
"url":"http://mp.weixin.qq.com",
"appid":"wx286b93c14bbf93aa",
"pagepath":"pages/lunar/index"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}]
}]
}

授权登陆

创建登陆页面,负责判断是否需要授权以及获取token等信息。

授权唤起页面:首页(无token) > 登陆页 > 授权页(微信官方) > 登陆页 > 首页。

详细流程:

  • 用户尝试进入首页面(其他需要授权的页面),在路由守卫判断token,不存在跳转到登陆页。

    router.beforeEach((to) => {
    if (to.meta.requireAuth && !storage.get(ACCESS_TOKEN)) {
    return {
    path: '/login',
    query: { redirect: to.fullPath },
    };
    }
    });
  • 进入登陆页,尝试获取 URL 中的code参数,如果有参数,发起请求尝试获取token;没有参数,尝试获取token,如果有则说明已经登陆过(浏览器返回上一页导致),没有则请求授权URL。

    let code = getRouteQuery().code;
    if (code) {
    getUserInfo(code).then((res) => {
    const { token, openId } = res;
    if (!token && openId) {
    // 未绑定
    bindPhoneVo.openId = openId;
    show.value = true;
    } else {
    // 记录token
    storage.set(ACCESS_TOKEN, res.token);
    const redirect = storage.get(LAND_PAGE) || '/';
    redirectTo(redirect);
    }
    });
    }
    function handleLogin() {
    const token = storage.get(ACCESS_TOKEN);
    // 记录上一个页面地址
    const { redirect } = getRouteQuery();
    if (redirect) {
    storage.set(LAND_PAGE, redirect);
    }
    if (!token) {
    // 跳转授权
    const REDIRECT_URI = window.location.href;
    const REDIRECT_URI_EC = encodeURIComponent(REDIRECT_URI);
    jump2Auth(REDIRECT_URI_EC).then((res) => {
    window.location.replace(res.redirectUrl);
    });
    }
    // 点击返回进入的该页面,此时无code
    redirectTo(redirect);
    }

完整的代码如下:

Controller:WechatController

Vue:login.vue

配置回调页面域名

公众平台 > 网页服务 > 网页账号 > 修改,我们回调的页面是前端的登陆页面,这里填写前端页面的URI。

注意不要写http

获取code

按照官方要求向其服务发送请求,携带redirect_url等参数,该请求会返回code等信息。

请求 URL 这里交给服务器进行拼接,Controller代码如下:

@GetMapping("/authorize")
public Map<String, String> authorize(@RequestParam("redirect_url") String returnUrl) {
String redirectURL = wxMpService.getOAuth2Service()
.buildAuthorizationUrl(URLDecoder.decode(returnUrl),
WxConsts.OAuth2Scope.SNSAPI_USERINFO,
null);
HashMap<String, String> map = new HashMap<>();
map.put("redirectUrl", redirectURL);
return map;
}

前端拿到redirectUrl之后,直接发起请求即可

const REDIRECT_URI = window.location.href;
const REDIRECT_URI_EC = encodeURIComponent(REDIRECT_URI);
jump2Auth(REDIRECT_URI_EC).then((res) => {
window.location.replace(res.redirectUrl);
});
  • 这里直接将redirect_url设置为登陆页,即请求成功后重定向到登陆页。

获取openId

有了code就可以获取openIduserInfo等信息:

@GetMapping("/userInfo")
public Map<String, String> userInfo(@RequestParam("code") String code) throws WxErrorException {
WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
String openId = accessToken.getOpenId();
WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(accessToken, null);

SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getOpenId, openId));
String token = null;
if (null != sysUser) {
// 已经绑定过了
token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
}
HashMap<String, String> map = new HashMap<>();
map.put("token", token);
map.put("openId", openId);
return map;
}

如果绑定过了,会携带token返回

前端通过是否有 token 判断是否绑定,唤起绑定功能:

getUserInfo(code).then((res) => {
const { token, openId } = res;
if (!token && openId) {
// 未绑定
bindPhoneVo.openId = openId;
show.value = true;
} else {
loading.value = false;
// 记录token
storage.set(ACCESS_TOKEN, res.token);
const redirect = storage.get(LAND_PAGE) || '/';
redirectTo(redirect);
}
});

消息推送

微信消息推送需要安装官方提供的模板,点击新增测试模板

模板设置

待处理审批

{{first.DATA}}
审批编号: {{keyword1.DATA}}
提交时间: {{keyword2.DATA}}
{{content.DATA}}

审批已处理

{{first.DATA}} 
审批编号:{{keyword1.DATA}}
提交时间:{{keyword2.DATA}}
审批状态:{{keyword3.DATA}}
当前审批人:{{keyword4.DATA}}
{{content.DATA}}

代码实现

MessageServiceImpl.java
@Service
public class MessageServiceImpl implements MessageService {

private final WxMpService wxMpService;
private final ProcessTemplateService processTemplateService;
private final SysUserService sysUserService;

@Value("${wechat.prefix}")
private String prefix;

public MessageServiceImpl(WxMpService wxMpService, ProcessTemplateService processTemplateService, SysUserService sysUserService) {
this.wxMpService = wxMpService;
this.processTemplateService = processTemplateService;
this.sysUserService = sysUserService;
}

/**
* 推送待审批人员
*
* @param process 流程ID
* @param user 用户ID
* @param taskId 任务ID
*/
@SneakyThrows
@Override
public void pushPendingMessage(Process process, SysUser user, String taskId) {
// 根据这些ID查询数据
ProcessTemplate processTemplate = processTemplateService.getOne(
new LambdaQueryWrapper<ProcessTemplate>()
.select(ProcessTemplate::getName)
.eq(ProcessTemplate::getId, process.getProcessTemplateId()));
SysUser submitUser = sysUserService.getOne(
new LambdaQueryWrapper<SysUser>()
.select(SysUser::getName)
.eq(SysUser::getId, LoginUserInfoHelper.getUserId()));
String openId = user.getOpenId();
if (openId == null) {
return;
}
// 构建模板消息
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId) // 消息接受人的openId
.templateId("PJaBg1zM5JeOsyc8T-2YVrYchQawCTT6etOhrpVequU") // 模板ID
.url(prefix + "/show/" + process.getId() + "/" + taskId + "/0")
.build();

JSONObject jsonObject = JSON.parseObject(process.getFormValues());
JSONObject formShowData = jsonObject.getJSONObject("formShowData");
StringBuilder content = new StringBuilder();
for (Map.Entry<String, Object> entry : formShowData.entrySet()) {
content.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}

// 设置模板变量值
templateMessage.addData(new WxMpTemplateData("first", submitUser.getName() + "提交" + processTemplate.getName() + "审批申请, 请注意查看。", "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword1", process.getProcessCode(), "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword2", new DateTime(process.getCreateTime()).toString("yyyy-MM-dd HH:mm:ss"), "#272727"));
templateMessage.addData(new WxMpTemplateData("content", content.toString(), "#272727"));

// 发送消息
wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
}

/**
* 推送给审批发起人
*
* @param process 流程ID
* @param status 任务ID
*/
@SneakyThrows
@Override
public void pushProcessedMessage(Process process, Integer status) {
ProcessTemplate processTemplate = processTemplateService.getOne(
new LambdaQueryWrapper<ProcessTemplate>()
.select(ProcessTemplate::getName)
.eq(ProcessTemplate::getId, process.getProcessTemplateId()));
SysUser user = sysUserService.getById(process.getUserId());
SysUser currentUser = sysUserService.getById(LoginUserInfoHelper.getUserId());
String openId = user.getOpenId();
if (openId == null) {
return;
}
// 构建模板消息
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId) // 消息接受人的openId
.templateId("FUinAm420s82h7nHRFB-cuZFgOxdVJTtv4faWn2xA74") // 模板ID
.url(prefix + "/show/" + process.getId() + "/0/0")
.build();

JSONObject jsonObject = JSON.parseObject(process.getFormValues());
JSONObject formShowData = jsonObject.getJSONObject("formShowData");
StringBuilder content = new StringBuilder();
for (Map.Entry<String, Object> entry : formShowData.entrySet()) {
content.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}

// 设置模板变量值
templateMessage.addData(new WxMpTemplateData("first", user.getName() + "你发起的" + processTemplate.getName() + "审批申请已经被处理了,请注意查看。", "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword1", process.getProcessCode(), "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword2", new DateTime(process.getCreateTime()).toString("yyyy-MM-dd HH:mm:ss"), "#272727"));
templateMessage.addData(new WxMpTemplateData("keyword3", status == 1 ? "审批通过" : "审批拒绝", status == 1 ? "#009966" : "#FF0033"));
templateMessage.addData(new WxMpTemplateData("keyword4", currentUser.getName(), "#272727"));
templateMessage.addData(new WxMpTemplateData("content", content.toString(), "#272727"));

// 发送消息
wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
}
}