1.  功能说明   100

员工端使用微信公众号完成审批操作,涉及到的功能包含:自定义菜单、授权登录、消息

1、微信公众号一级菜单为:审批列表、审批中心、我的

2、员工关注公众号,员工第一次登录微信公众号,通过微信授权登录进行员工账号绑定

3、员工通过微信公众号提交审批和审批信息,系统根据微信公众号推送审批信息,及时反馈审批过程

项目截图:

公众号菜单列表_List

2.  公众号菜单管理  100

公众号一级菜单,数据库默认初始化(审批列表、审批中心、我的)

页面效果如下:

公众号菜单列表_spring_02

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);

公众号菜单列表_spring_03

3. 菜单管理CRUD  100

老规矩利用代码生成器,生成代码

公众号菜单列表_公众号_04

公众号菜单列表_公众号_05

公众号菜单列表_List_06

操作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

公众号菜单列表_公众号_07

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

公众号菜单列表_spring_08

<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

公众号菜单列表_公众号_09

启动后端成功

公众号菜单列表_List_10

启动前端成功

公众号菜单列表_公众号_11

浏览器输入http://localhost:9528/

ok 没问题

公众号菜单列表_公众号_12

试试别的功能

添加

公众号菜单列表_spring_13

公众号菜单列表_spring_14

修改

公众号菜单列表_List_15

公众号菜单列表_公众号_16

删除

公众号菜单列表_公众号_17

公众号菜单列表_spring_18

没问题

4. 推送菜单  101

后台配置好菜单后,我们要推送到微信公众平台

公众号菜单列表_spring_19

4.1 申请账号  101

云尚办公系统没有微信支付等高级功能,因此无需使用服务号,使用测试账号即可完成测试。

我们使用“微信公众平台接口测试帐号”,申请地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login,以后有了正式账号,直接一切换即可

公众号菜单列表_List_20

扫描登录进入,获取测试号信息:appID与appsecret

公众号菜单列表_spring_21

然后微信扫码关注

公众号菜单列表_公众号_22

建议使用电脑端微信方便我们后续操作

公众号菜单列表_List_23

查看“自定义菜单“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添加

公众号菜单列表_List_24

syncMenu() {
  return request({
    url: `${api_name}/syncMenu`,
    method: `get`
  })
},

4.5.2 菜单列表添加同步功能

1、添加按钮

公众号菜单列表_List_25

<el-button class="btn-add" size="mini" @click="syncMenu" >同步菜单</el-button>

2、添加方法

公众号菜单列表_spring_26

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添加

公众号菜单列表_公众号_27

removeMenu() {
    return request({
      url: `${api_name}/removeMenu`,
      method: `delete`
    })
  }

4.7.2  菜单列表添加同步功能

1、添加按钮

公众号菜单列表_List_28

<el-button class="btn-add" size="mini" @click="removeMenu">删除菜单</el-button>

2、添加方法

公众号菜单列表_公众号_29

removeMenu() {
  menuApi.removeMenu().then(response => {
    this.$message.success('菜单已删除')
  })
}

4.8 测试  102

启动后端成功

公众号菜单列表_spring_30

启动前端成功

公众号菜单列表_spring_31

浏览器输入http://localhost:9528/

公众号菜单列表_公众号_32

点击同步菜单

公众号菜单列表_公众号_33

注意点击同步如果没有成功而是执行了全局异常,可能是因为你的微信的appsecret过期了,在刷新重新生成一个就好了

公众号菜单列表_公众号_34

看看公众号,没问题,成功

公众号菜单列表_List_35

试试删除

公众号菜单列表_spring_36

公众号菜单列表_spring_37

菜单消失,成功

公众号菜单列表_公众号_38