集成微信公众号

发布时间 2023-11-12 20:37:58作者: homle

 

1. 微信公众号菜单管理

  申请微信公众号获取appid和appsecret

  推送菜单有两种实现方式:

  完全按照接口文档http方式,但这种方式比较繁琐

  使用weixin-java-mp工具,这个是封装好的工具,可以直接使用,方便快捷,后续我们使用这种方式开发

  (1)在application-dev.yml中添加配置,引入依赖

wechat:
  mpAppId: wx13db7dcf69bc1223
  mpAppSecret: de3d7888d30febf84b64d0e6571e4027
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>4.1.0</version>
</dependency>

  (2)添加工具类和配置类

@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {

    private String mpAppId;

    private String mpAppSecret;

}
@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;
    }
}

  (3)接口实现

@ApiOperation(value = "同步菜单")
    @GetMapping("syncMenu")
    public Result createMenu() {
        menuService.syncMenu();
        return Result.ok();
    }

    @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://xxxxxxx.viphk.nnhk.cc/#"+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://xxxxxxx.viphk.nnhk.cc#"+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) {
            e.printStackTrace();
        }
    }
@ApiOperation(value = "删除菜单")
@DeleteMapping("removeMenu")
public Result removeMenu() {
    menuService.removeMenu();
    return Result.ok();
}
@SneakyThrows
@Override
public void removeMenu() {
    wxMpService.getMenuService().menuDelete();
}

  在管理界面上点击同步菜单或删除菜单就可以同步微信公众号栏的菜单

2. 微信授权登录

  当前后台员工账号与微信账号是没有关联的,因此在点击微信菜单时,要判断是否登录,如果是第一次访问则弹出关联层,建立微信账号与员工账号的绑定,即:通过员工手机号码与微信openId建立绑定,后续进入就知道用户身份了

  (1)配置内网穿透

  (2)配置授权回调页面域名

  在“[网页授权获取用户基本信息]”后面,点击“修改”,添加“授权回调页面域名”(本地使用内网穿透地址)

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

wechat:
  mpAppId: wx13db7dcf69bq1233
  mpAppSecret: de3d7888d30febf84b64d041231e4027
  # 授权回调获取用户信息接口地址
  userInfoUrl: http://ggkt2.vipgz1.91tunnel.com/admin/wechat/userInfo
@Controller
@RequestMapping("/admin/wechat")
@CrossOrigin
public class WechatController {

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private WxMpService wxMpService;

    @Value("${wechat.userInfoUrl}")
    private String userInfoUrl;

    /**
     * 微信授权,设置哪个路径需要获取微信信息,授权成功后的跳转路径
     */

    @GetMapping("/authorize")
    public String authorize(@RequestParam("returnUrl") String returnUrl, HttpServletRequest request) {
        //buildAuthorizationUrl三个参数
        //第一个参数:授权路径,在哪个路径下获取微信信息
        //第二个参数:固定值,授权类型 WxConsts.OAuth2Scope.SNSAPI_USERINFO
        //第三个参数:授权成功之后,跳转路径  'lw' 转换成  '#'
        String redirectUrl = null;
        System.out.println("au " + returnUrl);
        try {
            redirectUrl = wxMpService.getOAuth2Service()
                    .buildAuthorizationUrl(userInfoUrl,
                            WxConsts.OAuth2Scope.SNSAPI_USERINFO,
                            URLEncoder.encode(returnUrl.replace("lw", "#"), "utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        System.out.println("au " + redirectUrl);
        return "redirect:" + redirectUrl;
    }

    @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);
        //获取openId
        String openId = accessToken.getOpenId();
        System.out.println("openId" + openId);
        //获取用户信息
        WxOAuth2UserInfo userInfo = wxMpService.getOAuth2Service().getUserInfo(accessToken,null);
        System.out.println("userInfo" + JSON.toJSONString(userInfo));

        SysUser sysUser = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getOpenId, openId));
        String token = "";
        if (sysUser != null){
            token = JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());
        }
        if(returnUrl.indexOf("?") == -1) {
            return "redirect:" + returnUrl + "?token=" + token + "&openId=" + openId;
        } else {
            return "redirect:" + returnUrl + "&token=" + token + "&openId=" + openId;
        }
    }

    @PostMapping("bindPhone")
    @ResponseBody
    public Result bindPhone(@RequestBody BindPhoneVo bindPhoneVo) {
        //1 根据手机号查询数据库
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getPhone,bindPhoneVo.getPhone());
        SysUser sysUser = sysUserService.getOne(wrapper);

        //2 如果存在,更新记录 openid
        if(sysUser != null) {
            sysUser.setOpenId(bindPhoneVo.getOpenId());
            sysUserService.updateById(sysUser);

            String token = JwtHelper.createToken(sysUser.getId(),sysUser.getUsername());
            return Result.ok(token);
        } else {
            return Result.fail("手机号不存在,请联系管理员修改");
        }
    }
}

  a. /authorize

  参数returnUrl:授权成功后,返回路径

  设置回调接口路径(哪个路径的方法可以获取微信信息)

  b. /userInfo

  回调接口路径,在这个方法获取授权微信信息,获取openId,根据openId查询用户表,openId在表中是否已经存在

  c. /bindPhone

  微信用户绑定手机号

  根据微信使用绑定手机号查询用户表,信息是否存在,存在则更新openId值

  (4)排除拦截

  在WebSecurityConfig类配置排除拦截

   /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/admin/modeler/**","/diagram-viewer/**","/editor-app/**","/*.html",
                "/admin/processImage/**",
                "/admin/wechat/authorize","/admin/wechat/userInfo","/admin/wechat/bindPhone",
                "/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }

  (5)在移动端的页面都需要在授权登录后才可以访问。

<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 == '') {
          let url = window.location.href.replace('#', 'guiguoa')
          window.location = 'http://xxxxxx.cn/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.atguigu.cn'
      })
    },

    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>

  (6)配置前端拦截器

import axios from "axios";

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

// http request 拦截器
service.interceptors.request.use(config => {
    let token = window.localStorage.getItem("token") || "";
    if (token != "") {
      config.headers["token"] = token;
    }
    return config;
  },
  err => {
    return Promise.reject(err);
  });
// http response 拦截器
service.interceptors.response.use(response => {
    if (response.data.code == 208) {
      // debugger
      // 替换# 后台获取不到#后面的参数
      let url = window.location.href.replace('#', 'guiguoa')
      window.location = 'http://xxxxxxx.cn/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);   // 返回接口返回的错误信息
  });

export default service;

 关注微信号,授权登录后返回值

au http://xxxxoaweb.vipxxhk.nnhk.cc/lw/
au https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx4dd1dsxxxxb3c4a&redirect_uri=http%3A%2F%2Fxxxxoa.vipxxxhk.nnhk.cc%2Fadmin%2Fwechat%2FuserInfo&response_type=code&scope=snsapi_userinfo&state=http%3A%2F%2Fxxxxoaweb.vipxxhk.nnhk.cc%2F%23%2F&connect_redirect=1#wechat_redirect

code 061XLpFa1X6xxxxxxAGa1LePm93XLpFx
returnUrl http://xxxxxxoaweb.viphk.nnhk.cc/#/
openId oMWbp6LIFWxxxxxxbVY8B5VL9v2M

userInfo {"city":"","country":"","headImgUrl":"https://thxxixxrdwx.qlxxogo.cn/mmopen/vi_32/Q0j4TwGTfTLdrIFJmOuLJxxxVUAl5iaqficHFLx7NOCC7D97Bicia8ibTgcTvsNibOQEM3W0tqDoY6RXHwUtQJz9YnShNw/132","nickname":"未","openid":"oMWbp6xxxxxxVY8B5VL9v2M","privileges":[],"province":"","sex":0}

 

  3. 消息推送

  (1)配置微信模板消息

    待处理审批:{{first.DATA}} 审批编号:{{keyword1.DATA}} 提交时间:{{keyword2.DATA}} {{content.DATA}}

    审批已处理:{{first.DATA}} 审批编号:{{keyword1.DATA}} 提交时间:{{keyword2.DATA}} 当前审批人:{{keyword3.DATA}} 审批状态:{{keyword4.DATA}} {{content.DATA}}


@Slf4j
@Service
public class MessageServiceImpl implements MessageService {

    @Resource
    private WxMpService wxMpService;

    @Resource
    private ProcessService processService;

    @Resource
    private ProcessTemplateService processTemplateService;

    @Resource
    private SysUserService sysUserService;

    /**
     * 推送待审批人员
     * @param processId
     * @param userId
     * @param taskId
     */
    @SneakyThrows
    @Override
    public void pushPendingMessage(Long processId, Long userId, String taskId) {
        Process process = processService.getById(processId);
        ProcessTemplate processTemplate = processTemplateService.getById(process.getProcessTemplateId());
        SysUser sysUser = sysUserService.getById(userId);
        SysUser submitSysUser = sysUserService.getById(process.getUserId());
        String openid = sysUser.getOpenId();
        //方便测试,给默认值(开发者本人的openId)
        if(StringUtils.isEmpty(openid)) {
            openid = "omwf25izKON9dktgoy0dogqvnGhk";
        }
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
        .toUser(openid)//要推送的用户openid
        .templateId("KvOVeW7jz4-DZgQ_WuXjMZO5I4pPA7L7fflVNwC_ZQg")//模板id
        .url("http://oa.xxxxxx.cn/#/show/"+processId+"/"+taskId)//点击模板消息要访问的网址
        .build();
        JSONObject jsonObject = JSON.parseObject(process.getFormValues());
        JSONObject formShowData = jsonObject.getJSONObject("formShowData");
        StringBuffer content = new StringBuffer();
        for (Map.Entry entry : formShowData.entrySet()) {
            content.append(entry.getKey()).append(":").append(entry.getValue()).append("\n ");
        }
        templateMessage.addData(new WxMpTemplateData("first", submitSysUser.getName()+"提交了"+processTemplate.getName()+"审批申请,请注意查看。", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword1", process.getProcessCode(), "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword2", new DateTime(process.getCreateTime()).toString("yyyy-MM-dd HH:mm:ss"), "#272727"));
         templateMessage.addData(new WxMpTemplateData("content", content.toString(), "#272727"));
        String msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        log.info("推送消息返回:{}", msg);
    }

    @SneakyThrows
    @Override
    public void pushProcessedMessage(Long processId, Long userId, Integer status) {
        Process process = processService.getById(processId);
        ProcessTemplate processTemplate = processTemplateService.getById(process.getProcessTemplateId());
        SysUser sysUser = sysUserService.getById(userId);
        SysUser currentSysUser = sysUserService.getById(LoginUserInfoHelper.getUserId());
        String openid = sysUser.getOpenId();
        if(StringUtils.isEmpty(openid)) {
            openid = "omwf25izKON9dktgoy0dogqvnGhk";
        }
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                .toUser(openid)//要推送的用户openid
                .templateId("I0kVeto7T0WIDP6tyoHh-hx83wa9_pe7Nx9eT93-6sc")//模板id
                .url("http://oa.xxxxxx.cn/#/show/"+processId+"/0")//点击模板消息要访问的网址
                .build();
        JSONObject jsonObject = JSON.parseObject(process.getFormValues());
        JSONObject formShowData = jsonObject.getJSONObject("formShowData");
        StringBuffer content = new StringBuffer();
        for (Map.Entry entry : formShowData.entrySet()) {
            content.append(entry.getKey()).append(":").append(entry.getValue()).append("\n ");
        }
        templateMessage.addData(new WxMpTemplateData("first", "你发起的"+processTemplate.getName()+"审批申请已经被处理了,请注意查看。", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword1", process.getProcessCode(), "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword2", new DateTime(process.getCreateTime()).toString("yyyy-MM-dd HH:mm:ss"), "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword3", currentSysUser.getName(), "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword4", status == 1 ? "审批通过" : "审批拒绝", status == 1 ? "#009966" : "#FF0033"));
        templateMessage.addData(new WxMpTemplateData("content", content.toString(), "#272727"));
        String msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        log.info("推送消息返回:{}", msg);
    }

}

  (2)在启动流程后推送消息给下一个审批人,当审批人审批完成后也推送消息给申请人

@Override
    public void startUp(ProcessFormVo processFormVo) {
        SysUser sysUser = sysUserService.getById(LoginUserInfoHelper.getUserId());

        ProcessTemplate processTemplate = processTemplateService.getById(processFormVo.getProcessTemplateId());
        Process process = new Process();
        BeanUtils.copyProperties(processFormVo, process);
        String workNo = System.currentTimeMillis() + "";
        process.setProcessCode(workNo);
        process.setUserId(LoginUserInfoHelper.getUserId());
        process.setFormValues(processFormVo.getFormValues());
        process.setTitle(sysUser.getName() + "发起" + processTemplate.getName() + "申请");
        process.setStatus(1);
        baseMapper.insert(process);

        //绑定业务id
        String businessKey = String.valueOf(process.getId());
        //流程参数
        Map<String, Object> variables = new HashMap<>();
        //将表单数据放入流程实例中
        JSONObject jsonObject = JSON.parseObject(process.getFormValues());
        JSONObject formData = jsonObject.getJSONObject("formData");
        Map<String, Object> map = new HashMap<>();
        //循环转换
        for (Map.Entry<String, Object> entry : formData.entrySet()) {
            map.put(entry.getKey(), entry.getValue());
        }
        variables.put("data", map);
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processTemplate.getProcessDefinitionKey(), businessKey, variables);

        //业务表关联当前流程实例id
        String processInstanceId = processInstance.getId();
        process.setProcessInstanceId(processInstanceId);
        //计算下一个审批人,可能有多个(并行审批)
        List<Task> taskList = this.getCurrentTaskList(processInstanceId);
        if (!CollectionUtils.isEmpty(taskList)) {
            List<String> assigneeList = new ArrayList<>();
            for(Task task : taskList) {
                SysUser user = sysUserService.getByUsername(task.getAssignee());
                assigneeList.add(user.getName());

                //推送消息给下一个审批人,后续完善
                messageService.pushPendingMessage(process.getId(),user.getId(),task.getId());
            }
            process.setDescription("等待" + StringUtils.join(assigneeList.toArray(), ",") + "审批");
        }
        baseMapper.updateById(process);
        processRecordService.record(process.getId(), 1, "发起申请");
    }
public void approve(ApprovalVo approvalVo) {
    ...

    //计算下一个审批人
    Process process = this.getById(approvalVo.getProcessId());
    List<Task> taskList = this.getCurrentTaskList(process.getProcessInstanceId());
    if (!CollectionUtils.isEmpty(taskList)) {
        List<String> assigneeList = new ArrayList<>();
        for(Task task : taskList) {
            SysUser sysUser = sysUserService.getByUsername(task.getAssignee());
            assigneeList.add(sysUser.getName());

            //推送消息给下一个审批人
            messageService.pushPendingMessage(process.getId(), sysUser.getId(), task.getId());
        }

        process.setDescription("等待" + StringUtils.join(assigneeList.toArray(), ",") + "审批");
        process.setStatus(1);
    } else {
        ...
    }
    //推送消息给申请人
    messageService.pushProcessedMessage(process.getId(), process.getUserId(), approvalVo.getStatus());
    this.updateById(process);
}

  提交请假流程,审批人收到消息,查看消息详情页面