最近一直在看计算机视觉方面的论文,尽量抽时间复习下微服务吧。
微信自定义菜单文档地址:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
微信自定义菜单注意事项:
1. 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
2. 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。
3. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
一级菜单:直播、课程、我的
二级菜单:根据一级菜单动态设置二级菜单,直播(近期直播课程),课程(课程分类),我的(我的订单、我的课程、我的优惠券及关于我们)
说明:
1、二级菜单可以是网页类型,点击跳转H5页面
2、二级菜单可以是消息类型,点击返回消息
自定义菜单通过后台管理设置到数据库表,数据配置好后,通过微信接口推送菜单数据到微信平台。
表结构如下:
(1)页面功能“列表、添加、修改与删除”是对menu表的操作
(2)页面功能“同步菜单与删除菜单”是对微信平台接口操作
(1)在service下创建子模块service_wechat
(2)引入依赖
<dependencies>
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-mpartifactId>
<version>4.1.0version>
dependency>
dependencies>
(1)启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.atguigu")
@MapperScan("com.atguigu.ggkt.wechat.mapper")
@ComponentScan(basePackages = "com.atguigu")
public class ServiceWechatApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceWechatApplication.class, args);
}
}
(2)配置文件
# 服务端口
server.port=8305
# 服务名
spring.application.name=service-wechat
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/glkt_wechat?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath:com/atguigu/ggkt/wechat/mapper/xml/*.xml
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#公众号id和秘钥
# 硅谷课堂微信公众平台appId
wechat.mpAppId: 用你的
# 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 用你的
这里appId和密钥用你自己的,我也是用的别人的测试号,就不泄露了。
#service-wechat模块配置
#设置路由id
spring.cloud.gateway.routes[4].id=service-wechat
#设置路由的uri
spring.cloud.gateway.routes[4].uri=lb://service-wechat
#设置路由断言,代理servicerId为auth-service的/auth/路径
spring.cloud.gateway.routes[4].predicates= Path=/*/wechat/**
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController {
@Autowired
private MenuService menuService;
//公众号菜单删除
@DeleteMapping("removeMenu")
public Result removeMenu(){
menuService.removeMenu();
return Result.ok(null);
}
//同步菜单方法
@GetMapping("syncMenu")
public Result createMenu(){
menuService.syncMenu();
return Result.ok(null);
}
//获取access_token
@GetMapping("getAccessToken")
public Result getAccessToken(){
//拼接请求地址
StringBuffer buffer = new StringBuffer();
buffer.append("https://api.weixin.qq.com/cgi-bin/token");
buffer.append("?grant_type=client_credential");
buffer.append("&appid=%s");
buffer.append("&secret=%s");
//设置路径中的参数
String url = String.format(buffer.toString(),
ConstantPropertiesUtil.ACCESS_KEY_ID,
ConstantPropertiesUtil.ACCESS_KEY_SECRET);
try {
//发送http请求
String tokenString = HttpClientUtils.get(url);
//获取access_token
JSONObject jsonObject = JSONObject.parseObject(tokenString);
String access_token = jsonObject.getString("access_token");
//返回
return Result.ok(access_token);
} catch (Exception e) {
e.printStackTrace();
throw new GgktException(20001,"获取access_token失败");
}
}
//获取所有菜单,按照一级和二级菜单封装
@GetMapping("findMenuInfo")
public Result findMenuInfo(){
List<MenuVo> list=menuService.findMenuInfo();
return Result.ok(list);
}
//获取所有一级菜单
@GetMapping("findOneMenuInfo")
public Result findOneMenuInfo(){
List<Menu> list=menuService.findMenuOneInfo();
return Result.ok(list);
}
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
Menu menu = menuService.getById(id);
return Result.ok(menu);
}
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody Menu menu) {
menuService.save(menu);
return Result.ok(null);
}
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody Menu menu) {
menuService.updateById(menu);
return Result.ok(null);
}
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
menuService.removeById(id);
return Result.ok(null);
}
@ApiOperation(value = "根据id列表删除")
@DeleteMapping("batchRemove")
public Result batchRemove(@RequestBody List<Long> idList) {
menuService.removeByIds(idList);
return Result.ok(null);
}
}
(1)MenuService定义方法
public interface MenuService extends IService<Menu> {
//获取全部菜单
List<MenuVo> findMenuInfo();
//获取一级菜单
List<Menu> findOneMenuInfo();
}
(2)MenuServiceImpl实现方法
@Slf4j
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
@Autowired
private WxMpService wxMpService;
//获取所有菜单,按照一级和二级菜单封装
@Override
public List<MenuVo> findMenuInfo() {
//1、创建List集合,用于最终数据封装
List<MenuVo> finalMenuList=new ArrayList<>();
//2、查询出所有菜单数据(包含一级和二级)
List<Menu> menuList = baseMapper.selectList(null);
//3、从所有菜单数据中获取所有一级菜单数据(parent_id=0)
List<Menu> oneMenuList = menuList.stream()
.filter(menu -> menu.getParentId() == 0)
.collect(Collectors.toList());
//4、封装一级菜单数据,封装到最终数据list集合
//遍历一级菜单list集合
oneMenuList.forEach(oneMenu->{
//Menu -->MenuVo
MenuVo oneMenuVo = new MenuVo();
BeanUtils.copyProperties(oneMenu,oneMenuVo);
//5、封装二级菜单数据(判断一级菜单id和二级菜单的parent_id是否相同)
//如果相同,把二级菜单数据放到一级菜单里面
List<Menu> twoMenuList = menuList.stream()
.filter(menu -> menu.getParentId().equals(oneMenu.getId()))
.collect(Collectors.toList());
//List
List<MenuVo> children=new ArrayList<>();
twoMenuList.forEach(twoMenu -> {
MenuVo twoMenuVo = new MenuVo();
BeanUtils.copyProperties(twoMenu,twoMenuVo);
children.add(twoMenuVo);
});
//把二级菜单数据放到一级菜单里面
oneMenuVo.setChildren(children);
//把oneMenuVo放到最终list集合
finalMenuList.add(oneMenuVo);
});
//返回最终数据
return finalMenuList;
}
//获取所有一级菜单
@Override
public List<Menu> findMenuOneInfo() {
QueryWrapper<Menu> wrapper=new QueryWrapper<>();
wrapper.eq("parent_id",0);
List<Menu> list = baseMapper.selectList(wrapper);
return list;
}
//同步公众号菜单方法
//https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
//https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
@Override
public void syncMenu() {
//获取所有菜单数据
List<MenuVo> menuVoList = this.findMenuInfo();
//封装button里面的结构,数组格式
JSONArray buttonList=new JSONArray();
menuVoList.forEach(oneMenuVo -> {
//json对象 一级菜单
JSONObject one=new JSONObject();
one.put("name",oneMenuVo.getName());
//json数组 二级菜单
JSONArray subButton=new JSONArray();
oneMenuVo.getChildren().forEach(twoMenuVo->{
JSONObject view = new JSONObject();
view.put("type", twoMenuVo.getType());
if(twoMenuVo.getType().equals("view")) {
view.put("name", twoMenuVo.getName());
view.put("url", "http://ggkt2.vipgz1.91tunnel.com/#"
+twoMenuVo.getUrl());
} else {
view.put("name", twoMenuVo.getName());
view.put("key", twoMenuVo.getMeunKey());
}
subButton.add(view);
});
one.put("sub_button",subButton);
buttonList.add(one);
});
//封装最外层的button部分
JSONObject button=new JSONObject();
button.put("button",buttonList);
try {
String menuId =
this.wxMpService.getMenuService().menuCreate(button.toJSONString());
log.info("menuId:{}",menuId);
} catch (WxErrorException e) {
e.printStackTrace();
throw new GgktException(20001,"公众号菜单同步失败");
}
}
//公众号菜单删除
@Override
public void removeMenu() {
try {
wxMpService.getMenuService().menuDelete();
} catch (WxErrorException e) {
e.printStackTrace();
throw new GgktException(20001,"公众号菜单删除失败");
}
}
}
(1)进行菜单同步时候,需要获取到公众号的access_token,通过access_token进行菜单同步
接口文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
(2)调用方式
# 硅谷课堂微信公众平台appId
wechat.mpAppId: 用你的
# 硅谷课堂微信公众平台api秘钥
wechat.mpAppSecret: 用你的
@Component
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wechat.mpAppId}")
private String appid;
@Value("${wechat.mpAppSecret}")
private String appsecret;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = appid;
ACCESS_KEY_SECRET = appsecret;
}
}
/**
* 常量类,读取配置文件application.properties中的配置
*/
@Component
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wechat.mpAppId}")
private String appid;
@Value("${wechat.mpAppSecret}")
private String appsecret;
public static String ACCESS_KEY_ID;
public static String ACCESS_KEY_SECRET;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_ID = appid;
ACCESS_KEY_SECRET = appsecret;
}
}
//获取access_token
@GetMapping("getAccessToken")
public Result getAccessToken(){
//拼接请求地址
StringBuffer buffer = new StringBuffer();
buffer.append("https://api.weixin.qq.com/cgi-bin/token");
buffer.append("?grant_type=client_credential");
buffer.append("&appid=%s");
buffer.append("&secret=%s");
//设置路径中的参数
String url = String.format(buffer.toString(),
ConstantPropertiesUtil.ACCESS_KEY_ID,
ConstantPropertiesUtil.ACCESS_KEY_SECRET);
try {
//发送http请求
String tokenString = HttpClientUtils.get(url);
//获取access_token
JSONObject jsonObject = JSONObject.parseObject(tokenString);
String access_token = jsonObject.getString("access_token");
//返回
return Result.ok(access_token);
} catch (Exception e) {
e.printStackTrace();
throw new GgktException(20001,"获取access_token失败");
}
}
接口文档:https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
接口调用请求说明
http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
weixin-java-mp是封装好了的微信接口客户端,使用起来很方便,后续我们就使用weixin-java-mp处理微信平台接口。
@Configuration
public class WeChatMpConfig {
@Autowired
private ConstantPropertiesUtil constantPropertiesUtil;
@Bean
public WxMpService wxMpService(){
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage(){
WxMpDefaultConfigImpl wxMpConfigStorage = new WxMpDefaultConfigImpl();
wxMpConfigStorage.setAppId(ConstantPropertiesUtil.ACCESS_KEY_ID);
wxMpConfigStorage.setSecret(ConstantPropertiesUtil.ACCESS_KEY_SECRET);
return wxMpConfigStorage;
}
}
MenuService
void syncMenu();
MenuServiceImpl
//同步公众号菜单方法
//https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
//https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
@Override
public void syncMenu() {
//获取所有菜单数据
List<MenuVo> menuVoList = this.findMenuInfo();
//封装button里面的结构,数组格式
JSONArray buttonList=new JSONArray();
menuVoList.forEach(oneMenuVo -> {
//json对象 一级菜单
JSONObject one=new JSONObject();
one.put("name",oneMenuVo.getName());
//json数组 二级菜单
JSONArray subButton=new JSONArray();
oneMenuVo.getChildren().forEach(twoMenuVo->{
JSONObject view = new JSONObject();
view.put("type", twoMenuVo.getType());
if(twoMenuVo.getType().equals("view")) {
view.put("name", twoMenuVo.getName());
view.put("url", "http://ggkt2.vipgz1.91tunnel.com/#"
+twoMenuVo.getUrl());
} else {
view.put("name", twoMenuVo.getName());
view.put("key", twoMenuVo.getMeunKey());
}
subButton.add(view);
});
one.put("sub_button",subButton);
buttonList.add(one);
});
//封装最外层的button部分
JSONObject button=new JSONObject();
button.put("button",buttonList);
try {
String menuId =
this.wxMpService.getMenuService().menuCreate(button.toJSONString());
log.info("menuId:{}",menuId);
} catch (WxErrorException e) {
e.printStackTrace();
throw new GgktException(20001,"公众号菜单同步失败");
}
}
//同步菜单方法
@GetMapping("syncMenu")
public Result createMenu(){
menuService.syncMenu();
return Result.ok(null);
}
//公众号菜单删除
void removeMenu();
//公众号菜单删除
@Override
public void removeMenu() {
try {
wxMpService.getMenuService().menuDelete();
} catch (WxErrorException e) {
e.printStackTrace();
throw new GgktException(20001,"公众号菜单删除失败");
}
}
//公众号菜单删除
@DeleteMapping("removeMenu")
public Result removeMenu(){
menuService.removeMenu();
return Result.ok(null);
}
(1)src -> router -> index.js添加路由
{
path: '/wechat',
component: Layout,
redirect: '/wechat/menu/list',
name: 'Wechat',
meta: {
title: '菜单管理',
icon: 'el-icon-refrigerator'
},
alwaysShow: true,
children: [
{
path: 'menu/list',
name: 'Menu',
component: () => import('@/views/wechat/menu/list'),
meta: { title: '菜单列表' }
}
]
},
(1)src -> api -> wechat -> menu.js定义接口
import request from '@/utils/request'
const api_name = '/admin/wechat/menu'
export default {
findMenuInfo() {
return request({
url: `${api_name}/findMenuInfo`,
method: `get`
})
},
findOneMenuInfo() {
return request({
url: `${api_name}/findOneMenuInfo`,
method: `get`
})
},
save(menu) {
return request({
url: `${api_name}/save`,
method: `post`,
data: menu
})
},
getById(id) {
return request({
url: `${api_name}/get/${id}`,
method: `get`
})
},
updateById(menu) {
return request({
url: `${api_name}/update`,
method: `put`,
data: menu
})
},
syncMenu() {
return request({
url: `${api_name}/syncMenu`,
method: `get`
})
},
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: 'delete'
})
},
removeMenu() {
return request({
url: `${api_name}/removeMenu`,
method: `delete`
})
}
}
(1)创建views -> wechat -> menu -> list.vue
<template>
<div class="app-container">
<el-card class="operate-container" shadow="never">
<i class="el-icon-tickets" style="margin-top: 5px">i>
<span style="margin-top: 5px">数据列表span>
<el-button class="btn-add" size="mini" @click="remove" style="margin-left: 10px;">删除菜单el-button>
<el-button class="btn-add" size="mini" @click="syncMenu">同步菜单el-button>
<el-button class="btn-add" size="mini" @click="add">添 加el-button>
el-card>
<el-table
:data="list"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
default-expand-all
:tree-props="{children: 'children'}">
<el-table-column label="名称" prop="name" width="350">el-table-column>
<el-table-column label="类型" width="100">
<template slot-scope="scope">
{{ scope.row.type == 'view' ? '链接' : scope.row.type == 'click' ? '事件' : '' }}
template>
el-table-column>
<el-table-column label="菜单URL" prop="url" >el-table-column>
<el-table-column label="菜单KEY" prop="meunKey" width="130">el-table-column>
<el-table-column label="排序号" prop="sort" width="70">el-table-column>
<el-table-column label="操作" width="170" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.parentId > 0" type="text" size="mini" @click="edit(scope.row.id)">修改el-button>
<el-button v-if="scope.row.parentId > 0" type="text" size="mini" @click="removeDataById(scope.row.id)">删除el-button>
template>
el-table-column>
el-table>
<el-dialog title="添加/修改" :visible.sync="dialogVisible" width="40%" >
<el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
<el-form-item label="选择一级菜单">
<el-select
v-model="menu.parentId"
placeholder="请选择">
<el-option
v-for="item in list"
:key="item.id"
:label="item.name"
:value="item.id"/>
el-select>
el-form-item>
<el-form-item v-if="menu.parentId == 1" label="菜单名称">
<el-select
v-model="menu.name"
placeholder="请选择"
@change="liveCourseChanged">
<el-option
v-for="item in liveCourseList"
:key="item.id"
:label="item.courseName"
:value="item"/>
el-select>
el-form-item>
<el-form-item v-if="menu.parentId == 2" label="菜单名称">
<el-select
v-model="menu.name"
placeholder="请选择"
@change="subjectChanged">
<el-option
v-for="item in subjectList"
:key="item.id"
:label="item.title"
:value="item"/>
el-select>
el-form-item>
<el-form-item v-if="menu.parentId == 3" label="菜单名称">
<el-input v-model="menu.name"/>
el-form-item>
<el-form-item label="菜单类型">
<el-radio-group v-model="menu.type">
<el-radio label="view">链接el-radio>
<el-radio label="click">事件el-radio>
el-radio-group>
el-form-item>
<el-form-item v-if="menu.type == 'view'" label="链接">
<el-input v-model="menu.url"/>
el-form-item>
<el-form-item v-if="menu.type == 'click'" label="菜单KEY">
<el-input v-model="menu.meunKey"/>
el-form-item>
<el-form-item label="排序">
<el-input v-model="menu.sort"/>
el-form-item>
el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" size="small">取 消el-button>
<el-button type="primary" @click="saveOrUpdate()" size="small">确 定el-button>
span>
el-dialog>
div>
template>
<script>
import menuApi from '@/api/wechat/menu'
//import liveCourseApi from '@/api/live/liveCourse'
import subjectApi from '@/api/vod/subject'
const defaultForm = {
id: null,
parentId: 1,
name: '',
nameId: null,
sort: 1,
type: 'view',
meunKey: '',
url: ''
}
export default {
// 定义数据
data() {
return {
list: [],
liveCourseList: [],
subjectList: [],
dialogVisible: false,
menu: defaultForm,
saveBtnDisabled: false
}
},
// 当页面加载时获取数据
created() {
this.fetchData()
// this.fetchLiveCourse()
this.fetchSubject()
},
methods: {
// 调用api层获取数据库中的数据
fetchData() {
console.log('加载列表')
menuApi.findMenuInfo().then(response => {
this.list = response.data
console.log(this.list)
})
},
// fetchLiveCourse() {
// liveCourseApi.findLatelyList().then(response => {
// this.liveCourseList = response.data
// this.liveCourseList.push({'id': 0, 'courseName': '全部列表'})
// })
// },
fetchSubject() {
console.log('加载列表')
subjectApi.getChildList(0).then(response => {
this.subjectList = response.data
})
},
syncMenu() {
this.$confirm('你确定上传菜单吗, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return menuApi.syncMenu();
}).then((response) => {
this.fetchData()
this.$message.success(response.message)
}).catch(error => {
console.log('error', error)
// 当取消时会进入catch语句:error = 'cancel'
// 当后端服务抛出异常时:error = 'error'
if (error === 'cancel') {
this.$message.info('取消上传')
}
})
},
// 根据id删除数据
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return menuApi.removeById(id)
}).then((response) => {
this.fetchData(this.page)
if (response.code) {
this.$message({
type: 'success',
message: '删除成功!'
})
}
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
// -------------
add(){
this.dialogVisible = true
this.menu = Object.assign({}, defaultForm)
},
edit(id) {
this.dialogVisible = true
this.fetchDataById(id)
},
fetchDataById(id) {
menuApi.getById(id).then(response => {
this.menu = response.data
})
},
saveOrUpdate() {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.menu.id) {
this.saveData()
} else {
this.updateData()
}
},
// 新增
saveData() {
menuApi.save(this.menu).then(response => {
if (response.code) {
this.$message({
type: 'success',
message: response.message
})
this.dialogVisible = false;
this.fetchData(this.page)
}
})
},
// 根据id更新记录
updateData() {
menuApi.updateById(this.menu).then(response => {
if (response.code) {
this.$message({
type: 'success',
message: response.message
})
this.dialogVisible = false;
this.fetchData(this.page)
}
})
},
// 根据id查询记录
fetchDataById(id) {
menuApi.getById(id).then(response => {
this.menu = response.data
})
},
subjectChanged(item) {
console.info(item)
this.menu.name = item.title
this.menu.url = '/course/' + item.id
},
liveCourseChanged(item) {
console.info(item)
this.menu.name = item.courseName
if(item.id == 0) {
this.menu.url = '/live'
} else {
this.menu.url = '/liveInfo/' + item.id
}
}
}
}
script>
后端微服务启动
nacos查看服务是否注册成功。
前端项目启动
登录之后,点击公众号菜单管理->菜单列表
点击同步菜单
然后进入测试号中看是否同步成功。
菜单同步到这里就基本做完了。