1. 功能说明 100
员工端使用微信公众号完成审批操作,涉及到的功能包含:自定义菜单、授权登录、消息
1、微信公众号一级菜单为:审批列表、审批中心、我的
2、员工关注公众号,员工第一次登录微信公众号,通过微信授权登录进行员工账号绑定
3、员工通过微信公众号提交审批和审批信息,系统根据微信公众号推送审批信息,及时反馈审批过程
项目截图:
2. 公众号菜单管理 100
公众号一级菜单,数据库默认初始化(审批列表、审批中心、我的)
页面效果如下:
2.1 数据库设计 100
CREATE TABLE `wechat_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`parent_id` bigint(20) DEFAULT NULL COMMENT '上级id',
`name` varchar(50) DEFAULT NULL COMMENT '菜单名称',
`type` varchar(10) DEFAULT NULL COMMENT '类型',
`url` varchar(100) DEFAULT NULL COMMENT '网页 链接,用户点击菜单可打开链接',
`meun_key` varchar(20) DEFAULT NULL COMMENT '菜单KEY值,用于消息接口推送',
`sort` tinyint(3) DEFAULT NULL COMMENT '排序',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` tinyint(3) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='菜单';
#
# Data for table "wechat_menu"
#
INSERT INTO `wechat_menu` VALUES (2,0,'审批列表',NULL,NULL,NULL,1,'2022-12-13 09:23:30','2022-12-13 09:29:22',0),(3,0,'审批中心','view','/',NULL,2,'2022-12-13 09:23:44','2022-12-13 09:54:20',0),(4,0,'我的',NULL,NULL,NULL,3,'2022-12-13 09:23:52','2022-12-13 09:29:24',0),(5,2,'待处理','view','/list/0','',1,'2022-12-13 09:19:56','2022-12-13 09:24:10',0),(6,2,'已处理','view','/list/1','',2,'2022-12-13 09:27:00','2022-12-13 09:29:28',0),(7,2,'已发起','view','/list/1','',3,'2022-12-13 09:27:30','2022-12-13 09:29:30',0),(8,4,'基本信息','view','/user','',1,'2022-12-13 09:28:47','2022-12-13 09:28:47',0),(9,4,'关于我们','view','/about','',2,'2022-12-13 09:29:08','2022-12-13 09:29:32',0);
3. 菜单管理CRUD 100
老规矩利用代码生成器,生成代码
操作service-oa模块
3.1 mapper
package com.atguigu.wechat.mapper;
import com.atguigu.model.wechat.Menu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 菜单 Mapper 接口 100
* </p>
*/
public interface MenuMapper extends BaseMapper<Menu> {
}
3.2 service接口
package com.atguigu.wechat.service;
import com.atguigu.model.wechat.Menu;
import com.atguigu.vo.wechat.MenuVo;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* <p>
* 菜单 服务类 100
* </p>
*/
public interface MenuService extends IService<Menu> {
//菜单的层级显示 100
List<MenuVo> findMenuInfo();
}
3.3 service接口实现 100
package com.atguigu.wechat.service.impl;
import com.atguigu.model.wechat.Menu;
import com.atguigu.vo.wechat.MenuVo;
import com.atguigu.wechat.mapper.MenuMapper;
import com.atguigu.wechat.service.MenuService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* <p>
* 菜单 服务实现类 100
* </p>
*/
@Service
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
//菜单的层级显示 100
//获取全部菜单
@Override
public List<MenuVo> findMenuInfo() {
List<MenuVo> list = new ArrayList<>();
//1 查询所有菜单list集合
List<Menu> menuList = baseMapper.selectList(null);
//2 查询所有一级菜单 parent_id=0,返回一级菜单list集合
List<Menu> oneMenuList = menuList.stream()
.filter(menu -> menu.getParentId().longValue() == 0)
.collect(Collectors.toList());
//3 一级菜单list集合遍历,得到每个一级菜单
for(Menu oneMenu : oneMenuList) {
//一级菜单Menu 转为 MenuVo
//这里解释为什么要转,因为MenuVo属性必Menu更多更全,便于前端显示
MenuVo oneMenuVo = new MenuVo();
BeanUtils.copyProperties(oneMenu,oneMenuVo);
//4 获取每个一级菜单里面所有二级菜单 id 和 parent_id比较
//一级菜单id 和 其他菜单parent_id 一样的话就是改一级菜单的二级菜单
//这里利用stream流遍历我们得到的所有的菜单集合和我们得到的一级菜单集合作比较
List<Menu> twoMenuList = menuList.stream()
.filter(menu -> menu.getParentId().longValue() == oneMenu.getId())
.collect(Collectors.toList());
//5 把一级菜单里面所有二级菜单获取到,封装一级菜单children集合里面
//List<Menu> -- List<MenuVo>
List<MenuVo> children = new ArrayList<>();
for(Menu twoMenu : twoMenuList) {
MenuVo twoMenuVo = new MenuVo();
BeanUtils.copyProperties(twoMenu,twoMenuVo);
children.add(twoMenuVo);
}
//把二级菜单放入到它相应的一级菜单
oneMenuVo.setChildren(children);
//把每个封装好的一级菜单放到最终list集合
list.add(oneMenuVo);
}
return list;
}
}
3.4 controller接口 100
用于列表层级显示的实体类
package com.atguigu.wechat.controller;
import com.atguigu.common.result.Result;
import com.atguigu.model.wechat.Menu;
import com.atguigu.vo.wechat.MenuVo;
import com.atguigu.wechat.service.MenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 微信公众号 菜单 前端控制器 100
* </p>
*/
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController {
@Autowired
private MenuService menuService;
//菜单的层级显示 100
@ApiOperation(value = "获取全部菜单")
@GetMapping("findMenuInfo")
public Result findMenuInfo() {
List<MenuVo> menuList = menuService.findMenuInfo();
return Result.ok(menuList);
}
//以下自己实现,进攻参考 100
//@PreAuthorize("hasAuthority('bnt.menu.list')")
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
Menu menu = menuService.getById(id);
return Result.ok(menu);
}
//@PreAuthorize("hasAuthority('bnt.menu.add')")
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody Menu menu) {
menuService.save(menu);
return Result.ok();
}
//@PreAuthorize("hasAuthority('bnt.menu.update')")
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody Menu menu) {
menuService.updateById(menu);
return Result.ok();
}
//@PreAuthorize("hasAuthority('bnt.menu.remove')")
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
menuService.removeById(id);
return Result.ok();
}
}
MenuController
package com.atguigu.wechat.controller;
import com.atguigu.common.result.Result;
import com.atguigu.model.wechat.Menu;
import com.atguigu.vo.wechat.MenuVo;
import com.atguigu.wechat.service.MenuService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 微信公众号 菜单 前端控制器 100
* </p>
*/
@RestController
@RequestMapping("/admin/wechat/menu")
public class MenuController {
@Autowired
private MenuService menuService;
//菜单的层级显示 100
@ApiOperation(value = "获取全部菜单")
@GetMapping("findMenuInfo")
public Result findMenuInfo() {
List<MenuVo> menuList = menuService.findMenuInfo();
return Result.ok(menuList);
}
//根据id获取菜单 100
//@PreAuthorize("hasAuthority('bnt.menu.list')")
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result get(@PathVariable Long id) {
Menu menu = menuService.getById(id);
return Result.ok(menu);
}
//新增菜单
//@PreAuthorize("hasAuthority('bnt.menu.add')")
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody Menu menu) {
menuService.save(menu);
return Result.ok();
}
//修改菜单
//@PreAuthorize("hasAuthority('bnt.menu.update')")
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody Menu menu) {
menuService.updateById(menu);
return Result.ok();
}
//删除菜单
//@PreAuthorize("hasAuthority('bnt.menu.remove')")
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
menuService.removeById(id);
return Result.ok();
}
}
3.5 前端实现 101
3.5.1 定义api接口
注意我们写的使管理端,所以前端使guigu-oa-admin
创建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`
})
},
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
})
},
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: 'delete'
})
}
}
3.5.2 页面实现 101
创建views/wechat/menu/list.vue
<template>
<div class="app-container">
<!-- 工具条 -->
<div class="tools-div">
<el-button class="btn-add" size="mini" @click="add">添 加</el-button>
</div>
<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 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'
const defaultForm = {
id: null,
parentId: 1,
name: '',
nameId: null,
sort: 1,
type: 'view',
meunKey: '',
url: ''
}
export default {
// 定义数据
data() {
return {
list: [],
dialogVisible: false,
menu: defaultForm,
saveBtnDisabled: false
}
},
// 当页面加载时获取数据
created() {
this.fetchData()
},
methods: {
// 调用api层获取数据库中的数据
fetchData() {
console.log('加载列表')
menuApi.findMenuInfo().then(response => {
this.list = response.data
console.log(this.list)
})
},
// 根据id删除数据
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return menuApi.removeById(id)
}).then((response) => {
this.fetchData(this.page)
this.$message.success(response.message || '删除成功')
}).catch(() => {
this.$message.info('取消删除')
})
},
// -------------
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 => {
this.$message.success(response.message || '操作成功')
this.dialogVisible = false
this.fetchData(this.page)
})
},
// 根据id更新记录
updateData() {
menuApi.updateById(this.menu).then(response => {
this.$message.success(response.message || '操作成功')
this.dialogVisible = false
this.fetchData(this.page)
})
}
}
}
</script>
3.6 测试 101
记得修改我们的mp,因为我们之前移动了启动类的位置所以,需要添加mapper,使之能够找到mapper
启动后端成功
启动前端成功
ok 没问题
试试别的功能
添加
修改
删除
没问题
4. 推送菜单 101
后台配置好菜单后,我们要推送到微信公众平台
4.1 申请账号 101
云尚办公系统没有微信支付等高级功能,因此无需使用服务号,使用测试账号即可完成测试。
我们使用“微信公众平台接口测试帐号”,申请地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login,以后有了正式账号,直接一切换即可
扫描登录进入,获取测试号信息:appID与appsecret
然后微信扫码关注
建议使用电脑端微信方便我们后续操作
查看“自定义菜单“api文档:
https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
推送菜单有两种实现方式:
1、完全按照接口文档http方式,但这种方式比较繁琐
2、使用weixin-java-mp工具,这个是封装好的工具,可以直接使用,方便快捷,后续我们使用这种方式开发
4.2 添加配置 102
在application-dev.yml添加配置
wechat:
mpAppId: wx13db7dcf69bc1223
mpAppSecret: de3d7888d30febf84b64d0e6571e4027
4.3 工具类方式
4.3.1 引入依赖
操作service-oa模块
pom.xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.1.0</version>
</dependency>
4.3.2 添加工具类和配置类
操作service-oa模块
工具类WechatAccountConfig
package com.atguigu.wechat.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
//微信公众号推送的工具实体类 102
@Data //可以生成get set方法
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
//这个类的作用就是读取配置里面的以下两个值
private String mpAppId;
private String mpAppSecret;
}
配置类WeChatMpConfig
package com.atguigu.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;
//微信公众号推送配置类 102
@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;
}
}
4.4 推送接口实现 102
操作service-oa模块
业务层接口
操作类:MenuService
//同步菜单接口 102
void syncMenu();
业务层接口实现类 102
MenuServiceImpl
package com.atguigu.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;
//微信公众号推送配置类 102
@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;
}
}
controller接口
//同步菜单接口 102
@ApiOperation(value = "同步菜单")
@GetMapping("syncMenu")
public Result createMenu() {
menuService.syncMenu();
return Result.ok();
}
4.5 前端实现 102
4.5.1 api接口
在api/wechat/menu.js添加
syncMenu() {
return request({
url: `${api_name}/syncMenu`,
method: `get`
})
},
4.5.2 菜单列表添加同步功能
1、添加按钮
<el-button class="btn-add" size="mini" @click="syncMenu" >同步菜单</el-button>
2、添加方法
syncMenu() {
this.$confirm('你确定上传菜单吗, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
return menuApi.syncMenu()
}).then((response) => {
this.$message.success(response.message)
}).catch(error => {
console.log('error', error)
if (error === 'cancel') {
this.$message.info('取消上传')
}
})
}
4.6 删除推送菜单接口 102
操作service-oa模块
service接口
MenuService
//删除推送菜单 102
void removeMenu();
service接口实现
MenuServiceImpl
//删除推送菜单 102
@Override
public void removeMenu() {
try {
wxMpService.getMenuService().menuDelete();
} catch (WxErrorException e) {
throw new RuntimeException(e);
}
}
controller接口
//删除推送菜单 102
@ApiOperation(value = "删除菜单")
@DeleteMapping("removeMenu")
public Result removeMenu() {
menuService.removeMenu();
return Result.ok();
}
4.7 前端实现 102
4.7.1 api接口
在api/wechat/menu.js添加
removeMenu() {
return request({
url: `${api_name}/removeMenu`,
method: `delete`
})
}
4.7.2 菜单列表添加同步功能
1、添加按钮
<el-button class="btn-add" size="mini" @click="removeMenu">删除菜单</el-button>
2、添加方法
removeMenu() {
menuApi.removeMenu().then(response => {
this.$message.success('菜单已删除')
})
}
4.8 测试 102
启动后端成功
启动前端成功
点击同步菜单
注意点击同步如果没有成功而是执行了全局异常,可能是因为你的微信的appsecret过期了,在刷新重新生成一个就好了
看看公众号,没问题,成功
试试删除
菜单消失,成功