目录
最近在写web项目的过程中接触到公众号开发,在这码一码整个开发的流程
一、配置微信公众号测试号
微信测试号平台链接微信公众平台微信公众平台,给个人、企业和组织提供业务服务与用户管理能力的全新服务平台。https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
扫描登录进入,获取测试号信息:appID与appsecret,这两个很重要!!页面下方的二维码就是我们本次项目中使用到的测试号,可以自己扫码关注一下
如下是官方的开发文档,感兴趣的可以看看 微信公众平台开发概述 | 微信开放文档 (qq.com)https://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内网转发一条命令解决的外网访问内网问题,无需任何配置,下载客户端之后直接一条命令让外网访问您的内网不再是距离https://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); // 返回接口返回的错误信息
});
由于篇幅过长,我将会在下一篇中讲述如何进行公众号消息推送,感谢各位看到这里🎉