企业微信审批:记一次springboot集成企业微信审批流程

写在前面:
上周在工作中遇到了一个长SQL实现的报表,因为看到里面涉及到的sql语法,函数有几个常用的,有了不少新的粉丝,感谢感谢.下面时记录一次企业审批的过程,按照自定义的订单审批为例子,感觉可以覆盖大部分自定义审批模板,希望能给大家带来一些帮助,特别是项目准备要用企业审批的,能减少一些弯路.下面设计慕名,代码相关的,均为自定义,需要自己进行适当修改后方可食用.闲话不说,进入正文.

本文旨在给大家提供思路,并且是经过验证的思路,设计项目代码部分,会进行简单修改.编码不符合大众编码习惯的部分,还请大家见谅.
官网文档,开发中遇到问题,先在官网寻找答案

一、预处理

主要是发起审批前的一些校验工作,这个需要根据实际业务需求进行校验.然后记得将审批号与状态更新到订单

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean orderConfirm(OrderVO orderVO) {
        OrderVO order = orderService.getDetailById(orderVO.getId());
        // TODO 判断是否是新建状态 或者审核驳回状态
        // TODO 判断是否有仓库  经销商下单没有仓库 会导致后续销售出库单无法查询出来
        // TODO 判断是否有有结算方式  经销商下单没有结算方式
        // TODO 获取订单产品列表
        List<OrderProd> orderProdList = orderProdService.list(Wrappers.<OrderProd>lambdaQuery()
                .eq(OrderProd::getOrderId, order.getId()));
        
        for (OrderProd orderProd : orderProdList) {
            // TODO 判断可用库存是否充足
            // TODO 锁定库存
            // TODO 可以使用创建一个锁定库存表,插入数据的方式来锁定库存
        }

        // 1. TODO 更新客户余额  此处相当于预扣款
        // 推送企业微信审核:配置存在,且允许推送企业微信
        if (Func.isNotEmpty(wechatProperties.getEnable()) && wechatProperties.getEnable()){
            //推送企业微信
            String spNo = bizWeChatApply.orderApply(order);
            //获取微信审批号,用于后面回调,查看审批状态:通过,驳回等
            order.setWxSpNo(spNo);
            //  TODO 根据余额,做催款提醒
        }
        //设置为已提交,审核的状态为1
        order.setOrderStatus(IntPool.ONE);
        //更新审批状态与审批号
        return orderService.updateById(order);
    }

二、提交审批参数实体类

因为审批参数比较多,所以使用一个Java对象进行封装,同时对于一些默认的基本不会改的可以进行初始化,类似于约定大于配置吧.当然所有参数除了与订单相关的,都进行配置文件配置也是一种选择. 个人觉得配置太多可能会使得配置文件臃肿.取决于具体业务.如果发现默认的配置后期需要改动再重新放打扫配置文件也不是不可以.

/**
 * 企业微信提交审批参数实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WechatCommitParam implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 申请人注册手机号,此审批申请将以此员工身份提交,申请人需在应用可见范围内
     */
    private String phone;
    /**
     * 模板的唯一标识id。当前用来标识订单模板.同是订单审批,也可以根据环境等参数使用不同模板
     */
    private String templateId;
    /**
     * 审批人模式:0-通过接口指定审批人、抄送人(此时approver、notifyer等参数可用); 1-使用此模板在管理后台设置的审批流程,支持条件审批。
     */
    private Integer useTemplateApprover = 1;
    /**
     * 抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
     */
    private Integer notifyType = 2;
    /**
     * 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
     */
    private String[] summaryInfo;
    /**
     * 审批申请数据,可定义审批申请中各个控件的值,其中必填项必须有值,选填项可为空,数据结构同“获取审批申请详情”接口返回值中同名参数“apply_data”
     * 两个文本
     */
    private String[] applyData;
    /**
     * 附件
     * mediaId 获取接口 WeChatApproveUtil.getTempMediaId()
     */
    private List<WechatAttachment> mediaFileList;
}

三、审批方法之参数准备

这里主要是对参数进行格式化.按照企业微信官方文档以及实际业务对数据进行封装.这里只是对基本类型进行封装,对于表格数据还是放到调用者哪里

    /**
     * 订单审批推送
     * @param orderVO 订单信息,包含基础信息,顶打死你产品列表等
     * @return java.lang.String 审批号
     */
    @Override
    public String orderApply(OrderVO orderVO) {
        // 企微提交审批参数
        WechatCommitParam param = new WechatCommitParam();
        // 用于确定发起人
        param.setPhone(this.getUserPhone(orderVO.getSalesmanId()));
        // 通过环境与参数,比如产品列表个数,开发环境还是生产环境等,该ID可以配置在配置文件
        param.setTemplateId(template_order_prod);

        // 摘要信息,就是收到推送信息后,看到的简要信息
        String[] summaryInfo = new String[3];
        summaryInfo[0] = "订单审批";
        summaryInfo[1] =  orderVO.getOrderNo();
        summaryInfo[2] =  "客户: " + orderVO.getCustomerName();
        param.setSummaryInfo(summaryInfo);
        // 内容 订单编号 客户名称 收货地址 备注
        String[] applyData = new String[7];
        applyData[0] = orderVO.getOrderNo();
        applyData[1] = Func.isEmpty(orderVO.getStoreName()) ? "无" : orderVO.getStoreName();
        applyData[2] = orderVO.getCustomerName();
        applyData[3] = orderVO.getReceiverName();
        applyData[4] = orderVO.getReceiverPhone();
        applyData[5] = orderVO.getReceiverInfo();
        applyData[6] = orderVO.getRemark();
        param.setApplyData(applyData);
        return weChatApproveUtil.commitApproveApplyForOrder(param, orderVO.getOrderProdList(), orderVO.getOrderAuxList());
    }

四、审批方法

将API的基本参数设置好,对于表格类型,单独写方法进行封装,这主要是为了后期维护方便.该解耦的方法一定要解耦.个人曾经就看到有上千行代码的方法,当时是一阵头大.

    /**
     * 订单提交审批
     * @param commitParam   审批参数
     * @return  提交结果
     */
    public String commitApproveApplyForOrder(WechatCommitParam commitParam, List<OrderProdVO> orderProdList, List<OrderAux> orderAuxList) {
        // TODO 校验审批提交人与审批模板
        //真正调用API的参数构造
        Map<String, Object> reqParam = new HashMap<>();
        // 发起人
        reqParam.put("creator_userid", this.getUserId(commitParam.getPhone()));
        // 审批模板
        reqParam.put("template_id", commitParam.getTemplateId());
        // 审批人模式:0-通过接口指定审批人、抄送人  1-使用模板设置
        reqParam.put("use_template_approver", commitParam.getUseTemplateApprover());
        // 抄送方式:1-提单时抄送(默认值); 2-单据通过后抄送;3-提单和单据通过后抄送。仅use_template_approver为0时生效。
        reqParam.put("notify_type", commitParam.getNotifyType());
        // 摘要信息,用于显示在审批通知卡片、审批列表的摘要信息,最多3行
        reqParam.put("summary_list", this.setSummaryInfo(commitParam.getSummaryInfo()));
        // 审批申请数据装配
        JSONObject contents = this.getApplyDataForOrder(orderProdList, orderAuxList, commitParam.getApplyData());
        reqParam.put("apply_data", contents);

        // 发起审批
        String url = "https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN";
        JSONObject commit = this.doPost(url, reqParam, JSONObject.class);
        if (commit.getInteger("errcode").equals(IntegerPool.ZERO)) {
            return commit.getString("sp_no");
        }
        throw new ServiceException("提交审批失败: " + commit.getString("errmsg"));
    }

五、审批参数之参数构建

这部分就是对调用API的参数进行构建,特别需要核对控件ID与内容的一致性,避免使用SET,以免乱序导致的显示错行.

    /**
     * 根据模板填充审批数据 (订单) 正式环境
     * @param applyData     审批数据内容
     * @return JSONObject
     */
    private JSONObject getApplyDataForProdOrder(List<OrderProdVO> orderProdList, List<OrderAux> orderAuxList, String... applyData){

        JSONObject contents = new JSONObject();
        // 订单基本信息
        List<String> idList = new ArrayList<>();
        idList.add("Text-11111111111111"); // 订单编号
        idList.add("Text-22222222222222"); // 发货仓库
        idList.add("Text-33333333333333"); // 客户名称
        idList.add("Text-44444444444444"); // 联系人
        idList.add("Text-55555555555555"); // 联系方式
        idList.add("Text-66666666666666"); // 收货地址
        idList.add("Text-77777777777777"); // 备注

        JSONArray contentArray = new JSONArray();
        for (int i = 0; i < idList.size() && i < applyData.length; i++){
            JSONObject content = this.createContent(idList.get(i), applyData[i]);
            contentArray.add(content);
        }

        // 订单明细
        JSONObject prodTableValue = new JSONObject();
        JSONArray prodChildren = new JSONArray();
        if (Func.isNotEmpty(orderProdList)) {
            for (OrderProdVO orderProd : orderProdList) {
                JSONObject list = new JSONObject();
                JSONArray row = new JSONArray();
                // Text-1212121212 产品名称
                JSONObject prodName = new JSONObject();
                prodName.put("text", orderProd.getProdNameStr);
                row.add(this.createControl("Text-1212121212", "Text", prodName));
                
                //Money-23434343434 单价(元/袋) prodPrice
                JSONObject price = new JSONObject();
                price.put("new_money", orderProd.getProdPrice().stripTrailingZeros().toPlainString());
                row.add(this.createControl("Money-23434343434", "Money", price));

                // Number-454545454545数量(袋)
                JSONObject num = new JSONObject();
                num.put("new_number", orderProd.getProdNum().stripTrailingZeros().toPlainString());
                row.add(this.createControl("Number-454545454545", "Number", num));
                
                // Formula-222222222222222小计
                row.add(this.createControl("Formula-222222222222222", "Formula", new JSONObject()));

                list.put("list", row);
                prodChildren.add(list);
            }
            prodTableValue.put("children", prodChildren);
            JSONObject prodTable = new JSONObject();
            prodTable.put("control", "Table");
            prodTable.put("id", "Table-8888888888");
            prodTable.put("value", prodTableValue);
            contentArray.add(prodTable);
        }

        contents.put("contents", contentArray);
        return contents;
    }

六、控件构建

不同的控件用于显示不同数据,控件包含Text,Number,Money,Formula等,用于不同用途.具体用法参考官方文档,写的听详细了.

    /**
     * 创建Control
     * @param id        控件ID
     * @param control   控件类型
     * @param value     内容
     * @return content
     */
    private JSONObject createControl(String id, String control, JSONObject value){
        JSONObject content = new JSONObject();
        content.put("control", control);
        content.put("id", id);
        content.put("value", value);
        return content;
    }

七、请求

将token换成真实token,去发起请求

    /**
     * post请求
     * @param url       请求地址
     * @param reqParam  请求参数
     * @param tClass    请求返回值转换类型
     */
    private <T> T doPost(String url, Map<String, Object> reqParam, Class<T> tClass){
        String token = this.getToken();
        String token_url = url.replace("ACCESS_TOKEN", token);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<T> responseEntity = restTemplate.postForEntity(token_url, reqParam, tClass);
        //log.info("doPost请求地址 ==> {}", url);
        log.info("doPost请求参数 ==> {}", reqParam);
        //log.info("doPost获取结果 ==> {}", responseEntity);
        if (responseEntity.getStatusCode() == HttpStatus.OK) {
            return responseEntity.getBody();
        }
        throw new ServiceException(String.format("Post请求%s失败", url));
    }

八、获取token

1.通过缓存获取token
    /**
     * 缓存Token
     * @return Token
     */
    public String getToken() {
        // 先查询缓存 没有则请求接口
        String redisTokenKey = "ACCESS_TOKEN_YL_SALE_TEST"; 
        String tokenValue = redisCacheWechat.getCacheObject(redisTokenKey);
        if (Func.isBlank(tokenValue)) {
            Integer timeOut = 7200;
            tokenValue = getAccessToken();
            redisCacheWechat.setCacheObject(redisTokenKey, tokenValue, timeOut, TimeUnit.SECONDS);
            log.info("接口请求Token");
        }
        return tokenValue;
    }
2.通过接口获取token
    /**
     * 直接请求Token
     * @return Token
     */
    private String getAccessToken() {
        String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET"
            .replace("ID", wechatProperties.getCorpId()).replace("SECRET", wechatProperties.getCorpSecret());
        AccessTokenResult ac = this.doGet(url, AccessTokenResult.class);
        log.info("获取企业微信Token结果 ==> {}", ac);
        if (ac != null && ac.getErrcode() == 0) {
            return ac.getAccess_token();
        }
        String error = ac == null ? "body is null" : ac.getErrmsg();
        throw new ServiceException("获取微信token失败: " + error);
    }
3.GET请求通用方法
    /**
     * get请求
     * @param url       请求地址
     * @param tClass    请求返回值转换类型
     */
    private <T> T doGet(String url, Class<T> tClass){
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<T> responseEntity = restTemplate.getForEntity(url, tClass);
        log.info("doGet请求地址 ==> {}", url);
        log.info("doGet获取结果 ==> {}", responseEntity);
        if (HttpStatus.OK == responseEntity.getStatusCode()) {
            return responseEntity.getBody();
        }
        throw new ServiceException(String.format("Get请求%s失败", url));
    }
4.缓存通用方法
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(prefix+":"+key, value, timeout, timeUnit);
    }

九、回调,更新订单状态

我们是通过定时任务回调来获取审批结果的

    /**
     * 获取订单审批结果
     * 状态 (0-新建; 1-已提交(企业微信待审核); 2-确认(企业微信审核同意); 3-部分发货; 4-发货完成; 5-订单完成; 6-审核驳回; 9-取消作废(已关闭))
     * 审批通过创建销售出库单
     */
    @Override
    public void orderTask() {
        List<Order> orderList = orderService.list(Wrappers.<Order>lambdaQuery()
                .eq(Order::getOrderStatus, IntegerPool.ONE)
                .eq(Order::getIsDeleted, IntegerPool.ZERO)
                .isNotNull(Order::getWxSpNo));
        if (orderList.isEmpty()){
            return;
        }
        List<Order> updateList = new ArrayList<>();
        for (Order order : orderList) {
            Integer spStatus = weChatApproveUtil.getApprovalDetail(order.getWxSpNo());
            // 审批通过
            if (spStatus.equals(IntegerPool.TWO)){
                // 更新订单状态
                order.setOrderStatus(IntegerPool.TWO);
                // TODO 创建销售出库单 已创建销售出库单的不继续执行

                // 推送金蝶
                if (kingDeeProperties.isEnable()) {
                    // TODO 推送金蝶逻辑
                }
                // 审核驳回
                if (spStatus.equals(IntegerPool.THREE)){
                    order.setOrderStatus(IntegerPool.SIX);
                    // 更新客户余额  相当于退货了
                    // 恢复客户预扣款金额
                    // 整单解锁库存
                    
                }
            }
            orderService.updateBatchById(updateList);
        }

回调函数,获取审批结果状态

    /**
     * 获取审批申请详情
     * sp_status 申请单状态:1-审批中;2-已通过;3-已驳回;4-已撤销;6-通过后撤销;7-已删除;10-已支付
     * @param spNo  审批单号
     * @return  sp_status
     */
    public Integer getApprovalDetail(String spNo) {
        String url = "https://qyapi.weixin.qq.com/cgi-bin/oa/getapprovaldetail?access_token=ACCESS_TOKEN";
        Map<String, Object> reqParam = new HashMap<>();
        reqParam.put("sp_no", spNo);
        JSONObject detail = this.doPost(url, reqParam, JSONObject.class);
        System.out.println(detail);
        if (detail.getInteger("errcode").equals(IntegerPool.ZERO)) {
            JSONObject info = detail.getJSONObject("info");
            return info.getInteger("sp_status");
        }
        throw new ServiceException("获取申请详情失败: " + detail.getString("errmsg"));
    }
企业微信截图_17329701028039.png
企业微信截图_17329702439136.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容