如何在项目中整合微信公众号开发 - 上篇

目录

一、配置微信公众号测试号

二、配置weixin-java-mp

2.1 在application-dev.yml中添加配置

2.2 引入依赖

2.3 添加工具类和配置类

三、添加自定义菜单

3.1 menuController中定义方法 

3.2 menuService中重写该方法 

四、 配置内网穿透

4.1 开通隧道(需实名认证)

4.2 配置“授权回调页面域名”

4.3 配置授权回调获取用户信息接口地址 

五、编写后端接口

六、前端修改(基于vue)


最近在写web项目的过程中接触到公众号开发,在这码一码整个开发的流程

一、配置微信公众号测试号

微信测试号平台链接微信公众平台微信公众平台,给个人、企业和组织提供业务服务与用户管理能力的全新服务平台。icon-default.png?t=N7T8https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
扫描登录进入,获取测试号信息:appID与appsecret,这两个很重要!!页面下方的二维码就是我们本次项目中使用到的测试号,可以自己扫码关注一下

如下是官方的开发文档,感兴趣的可以看看 微信公众平台开发概述 | 微信开放文档 (qq.com)icon-default.png?t=N7T8https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html在本项目中采用的是weixin-java-mp工具进行开发,下面对其进行配置。

二、配置weixin-java-mp

2.1 在application-dev.yml中添加配置

# 注意不是在spring下!!!
wechat:
  mpAppId: # 上面提到的两个属性复制粘贴进去
  mpAppSecret: 

2.2 引入依赖

在项目pom文件中引入依赖,记得更新重启一下项目😄

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>4.1.0</version>
</dependency>

2.3 添加工具类和配置类

工具类

package com.oa.wechat.config;

/**
 * @author Charles
 * @create 2023-05-15-13:05
 */

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "wechat") //读取配置配置中写的值
public class WechatAccountConfig {
    private String mpAppId;

    private String mpAppSecret;
}

配置类

package com.oa.wechat.config;

import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class WeChatMpConfig {

    @Autowired
    private WechatAccountConfig wechatAccountConfig;

    @Bean
    public WxMpService wxMpService(){
        WxMpService wxMpService = new WxMpServiceImpl();
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
        return wxMpService;
    }

    @Bean
    public WxMpConfigStorage wxMpConfigStorage(){
        WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
        wxMpConfigStorage.setAppId(wechatAccountConfig.getMpAppId());
        wxMpConfigStorage.setSecret(wechatAccountConfig.getMpAppSecret());
        return wxMpConfigStorage;
    }
}

后续通过这里配置的WxMpService来操作微信公众号

三、添加自定义菜单

一个公众号肯定少不了相关的菜单栏,这里步里会讲述如何添加自定义菜单
下图是微信官方指定的菜单格式,由于官方只认这一个格式,所以我们需要将从数据库获取到的数据转换成该格式

3.1 menuController中定义方法 

@PreAuthorize("hasAuthority('bnt.menu.syncMenu')") //权限控制
@ApiOperation(value = "同步菜单")
@GetMapping("syncMenu")
public Result syncMenu() {
    menuService.syncMenu();
    return Result.success();
}

3.2 menuService中重写该方法 

@Service
public void menuServiceImpl(){
    @Autowired
    private WxMpService wxMpService;

    //获取菜单树形结构
    @Override
    public List<MenuVo> findMenuInfo() {
        List<Menu> menus = weChatMenuMapper.selectList(null);

        //获取到所有的父级菜单
        List<Menu> menusParent = menus.stream().filter(menu -> menu.getParentId().longValue() == 0)
                .collect(Collectors.toList());

        List<MenuVo> menuVoList = new ArrayList<>();
        for(Menu menu : menusParent){
            MenuVo menuVo = new MenuVo();
            BeanUtils.copyProperties(menu,menuVo);

            //获取到所有的二级目录
            List<Menu> subMenuList = menus.stream().filter(myMenu -> myMenu.getParentId().longValue() == menu.getId())
                    .sorted(Comparator.comparing(Menu::getSort))
                    .collect(Collectors.toList());
            //遍历转换
            List<MenuVo> children = new ArrayList<>();
            for(Menu menu1 : subMenuList){
                MenuVo myMenuVo  = new MenuVo();
                BeanUtils.copyProperties(menu1,myMenuVo);
                children.add(myMenuVo);
            }

            menuVo.setChildren(children);
            menuVoList.add(menuVo);
        }

        return menuVoList;
    }
    
    @Override
    public void syncMenu() {
        //将数据库中存储的数据封装为层级关系
        List<MenuVo> menuVoList = this.findMenuInfo();
        //菜单
        JSONArray buttonList = new JSONArray();
        for(MenuVo oneMenuVo : menuVoList) {
            JSONObject one = new JSONObject();
            one.put("name", oneMenuVo.getName());
            if(CollectionUtils.isEmpty(oneMenuVo.getChildren())) {
                one.put("type", oneMenuVo.getType());
                one.put("url", "http://oa.charles.vip.tunnel/#"+oneMenuVo.getUrl());
            } else {
                JSONArray subButton = new JSONArray();
                for(MenuVo twoMenuVo : oneMenuVo.getChildren()) {
                    JSONObject view = new JSONObject();
                    view.put("type", twoMenuVo.getType());
                    if(twoMenuVo.getType().equals("view")) {
                        view.put("name", twoMenuVo.getName());
                        //H5页面地址 这里队应的是内网穿透的地址 后面会讲
                        view.put("url", "http://oa.charles.vip.tunnel/#"+twoMenuVo.getUrl())
                    } else {
                        view.put("name", twoMenuVo.getName());
                        view.put("key", twoMenuVo.getMeunKey());
                    }
                    subButton.add(view);
                }
                one.put("sub_button", subButton);
            }
            buttonList.add(one);
        }
        //菜单
        JSONObject button = new JSONObject();
        button.put("button", buttonList);
        try {
            //固定格式 调用方法来生成菜单
            wxMpService.getMenuService().menuCreate(button.toJSONString());
        } catch (WxErrorException e) {
            throw new RuntimeException(e);
        }
    }
}

 最终效果如下图所示

四、 配置内网穿透

前面我们完成配置公众号的菜单,当点击菜单的时候会跳转到我们本地的页面中,但是由于是内网,外网无法直接访问。这时候可以采用内网穿透技术或者ip来实现功能,当然注册域名更好😁 这里以ngrok为例子进行内网穿透 ngrok.cc内网转发一条命令解决的外网访问内网问题,无需任何配置,下载客户端之后直接一条命令让外网访问您的内网不再是距离icon-default.png?t=N7T8https://ngrok.ccngrok.cc

4.1 开通隧道(需实名认证)

配置好本地端口,由于我们有两个端口所以需要两条隧道(前端页面,后端接口)

开通成功后会看到隧道id,通过下载客户端然后启动当前隧道就ok了

4.2 配置“授权回调页面域名”

网页服务-网页账号-修改 处修改,配置的地址为后端接口的地址,比如我后端接口8080对应地址oa.charles.vip.tunnel.com (即复制该地址上去 注意无需前缀http://)

4.3 配置授权回调获取用户信息接口地址 

wechat:
      # 跟着前面配置过的myAppId等
  # 授权回调获取用户信息接口地址
  userInfoUrl: http://oa.charles.vip.tunnel.com/admin/wechat/userInfo

五、编写后端接口

在controller中我们需要定义三个方法,分别是
userInfo -- 获取用户信息 OpenId为微信的唯一标识

@GetMapping("/userInfo")
public String userInfo(@RequestParam("code") String code,
                       @RequestParam("state") String returnUrl) throws Exception {
    
    System.out.println("code " + code);
    System.out.println("returnUrl " + returnUrl);
    //获取认证token
    WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
    //从token中获取openId
    String openId = accessToken.getOpenId();

    //查找user表中是否存在该openId
    LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<SysUser>();
    lambdaQueryWrapper.eq(SysUser::getOpenId,openId);
    SysUser sysUser = sysUserService.getOne(lambdaQueryWrapper);

    //最终前端都是通过token来判断
    String token = "";
    //null != sysUser 说明已经绑定,反之为建立账号绑定,去页面建立账号绑定
    if(null != sysUser) {
        token = JWTHelper.createToken(sysUser.getId(), sysUser.getUsername());
        System.out.println("token " + token);
    }
    if(returnUrl.indexOf("?") == -1) {
        return "redirect:" + returnUrl + "?token=" + token + "&openId=" + openId;
    } else {
        return "redirect:" + returnUrl + "&token=" + token + "&openId=" + openId;
    }
}

authorize -- 授权登录 

@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
    //设置回调URL
    /* 1. 获取info信息地址
     *  2. 定义URL类型,这里是UserInfo
     *  3. 回调地址
    **/
    System.out.println("InfoUrl  "  + InfoUrl);
    System.out.println("returnUrl " + returnUrl);
    String redirectUrl = "";
    try {
        redirectUrl = wxMpService.getOAuth2Service().buildAuthorizationUrl(InfoUrl,
                WxConsts.OAuth2Scope.SNSAPI_USERINFO,
                URLEncoder.encode(returnUrl.replace("guiguoa", "#"), "utf-8"));
        System.out.println("微信网页端回调地址 "+redirectUrl);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    //跳转
    return "redirect:" + redirectUrl;
}

bindPhone -- 当成功授权后,如果微信号是第一次登录,需要提供窗口给用户用户手机号绑定 

@ApiOperation(value = "微信账号绑定手机")
@PostMapping("/bindPhone")
@ResponseBody
public Result bindPhone(@RequestBody BindPhoneVo bindPhoneVo) {
    String phone = bindPhoneVo.getPhone();

    //查找user表中是否有当前记录
    SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhone, phone));

    if(null != sysUser){
        //如果用户存在则绑定当前手机号
        sysUser.setOpenId(bindPhoneVo.getOpenId());
        sysUserService.updateById(sysUser);
    }else
        return Result.fail("找不到当前号码,请联系管理员");

    return Result.success(JWTHelper.createToken(sysUser.getId(),sysUser.getUsername()));
}

当然如果项目中配置了SpringSecurity 还需要在config中排除上述三个方法的路径

六、前端修改(基于vue)

前端的项目中主要修改两个地方,一个是src/App.vue 可以直接copy一下

<template>
  <div id="app">
    <router-view />
    <el-dialog title="绑定手机" :visible.sync="dialogVisible" width="80%" >
      <el-form ref="dataForm" :model="bindPhoneVo" size="small">
        <h4>绑定你的手机号</h4>
        <el-form-item label="手机号码">
          <el-input v-model="bindPhoneVo.phone"/>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" icon="el-icon-check" @click="saveBind()" size="small">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>

import userInfoApi from '@/api/userInfo'
export default {
  data() {
    return {
      show: true,
      dialogVisible: false,
      bindPhoneVo: {
        openId: '',
        phone: ''
      }
    };
  },
  
  created() {
    // 处理微信授权登录
    this.wechatLogin();
  },

  methods: {
    wechatLogin() {
      // 处理微信授权登录
      let token = this.getQueryString('token') || '';
      let openId = this.getQueryString('openId') || '';
      // token === '' && openId != '' 只要这种情况,未绑定账号
      if(token === '' && openId != '') {
        // 绑定账号
        this.bindPhoneVo.openId = openId
        this.dialogVisible = true
      } else {
        // 如果绑定了,授权登录直接返回token
        if(token !== '') {
          window.localStorage.setItem('token', token);
        }

        token = window.localStorage.getItem('token') || '';
        if (token == '') {
          window.location = 'http://oa.charles.vip.tunnel.com/admin/wechat/authorize?returnUrl=' + url
        }
      }
    },

    saveBind() {
      if(this.bindPhoneVo.phone.length != 11) {
        alert('手机号码格式不正确')
        return
      }

      userInfoApi.bindPhone(this.bindPhoneVo).then(response => {
        window.localStorage.setItem('token', response.data);
        this.dialogVisible = false
        window.location = 'http://oa.charles.free.idcfengye.com'
      })
    },

    getQueryString (paramName) {
      if(window.location.href.indexOf('?') == -1) return '';
      let searchString = window.location.href.split('?')[1];
      let i, val, params = searchString.split("&");
      for (i=0;i<params.length;i++) {
        val = params[i].split("=");
        if (val[0] == paramName) {
          return val[1];
        }
      }
      return '';
    }
  }
};
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

另一个是src/utils/request.js 

// 创建axios实例
const service = axios.create({
  baseURL: "http://oa.charles.vip.tunnel.com", // api 的 base_url
  timeout: 30000 // 请求超时时间
});

// http response 拦截器
service.interceptors.response.use(response => {
    if (response.data.code == 208) { //登录失败

      window.location = 'http://oa.charles.vip.tunnel.com/admin/wechat/authorize?returnUrl=' + url
    } else {
      if (response.data.code == 200) {
        return response.data;
      } else {
        // 209没有权限 系统会自动跳转授权登录的,已在App.vue处理过,不需要提示
        if (response.data.code != 209) {
          alert(response.data.message || "error");
        }
        return Promise.reject(response);
      }
    }
  },
  error => {
    return Promise.reject(error.response);   // 返回接口返回的错误信息
  });

 由于篇幅过长,我将会在下一篇中讲述如何进行公众号消息推送,感谢各位看到这里🎉

  • 14
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值