钉钉工作流对接:多个公司同一回调接口处理

相关资料(地址)

应用创建

- git clone https://github.com/opendingtalk/eapp-corp-project.git
  • 白名单设置
外网地址,自己查询设置
  • 添加接口权限
    接口权限

项目对接

  • 问题点

    • 回调地址注册

      ERP系统是多组织的(即多个数据库),钉钉公司(存在多个)和组织(数据库)之间是一对多的关系,即多个组织共用一个公司,但问题是一个公司只能注册一个回调接口,且回调接口接收的数据是加密的
      而且娜拉供应链的回调接口已经注册在了BUS项目中了,即还需要打通BUS到ERP的连接
      
      所以问题就变成:
      
          1. 如何通过一个回调接口来区分这次回调是属于哪个钉钉公司的
      
          2. 如何通过解密后的信息区分这次回调时属于那个组织的(切换到对应的数据源)
      
    • Excel文件的上传

      ERP是外部系统,无法使用附件组件上传文件到钉盘(钉钉不允许),但是允许提交图片地址
      
  • 解决思路

    • 回调地址注册
      1. 截取钉钉公司的唯一值CorpId的前15位(不可对外暴露,只能截取部分)+根据MD5(appkey+appSecret)的前21位,合成一个假的CorpId加入到回调地址末尾去注册
      
      2. 回调接口通过@PathVariable注解获取假得CorpId,然后截取出前15位去数据库中查询对应公司的应用凭证
      
      3. 通过真的CorpId对加密信息解密
      
      4. 获取processInstanceId,通过processInstanceId查询Redis,获取审批单对应的数据源
      
      5. 切换到对应的数据源进行后续处理
      
    • 图片上传
      1. 将生成Excel所需的数据传给前端,前端通过Canvas生成对应的图片
      
      2,后端接收图片并将图片上传到又拍云,获取图片Url,将图片地址再提交到钉钉审批
      
  • Code

    • 回调地址注册
    //启动注册环境条件
        public class RunableDependOnEnvCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
            String[] activeProfiles = conditionContext.getEnvironment().getActiveProfiles();
            String activeProfile = activeProfiles[0];
            return "prod".equals(activeProfile) || "test".equals(activeProfile);
    
        }
    }
    
    @Conditional(RunableDependOnEnvCondition.class)
    @Component
    @Slf4j
    public class InjectCallBackUrl implements CommandLineRunner {
    
        @Resource
        DingTalkConfig dingTalkConfig;
    
        @Resource
        private ISimbaCompanyService simbaCompanyService;
    
        /**
        * 回调地址注册
        *
        * @param args
        * @throws Exception
        */
        @Override
        public void run(String... args) throws Exception {
            log.info("项目启动完成,开始自动注册钉钉回调地址");
            DynamicDataSourceContextHolder.setDataSource(DataSourceType.TN, "");
            // 获取所有的钉钉公司
            List<SimbaCompany> simbaCompanies = simbaCompanyService.listDingTalkCompany();
            for (SimbaCompany simbaCompany : simbaCompanies) {
    
                //娜拉供应链不在ERP注册
                if (!"dingb9ce960xxxxxxxxxxxc2f4657eb6378f".equals(simbaCompany.getCorpId())) {
                    registerCallBackUrl(simbaCompany.getAppKey(), simbaCompany.getAppSecret(), simbaCompany.getCorpId());
                }
            }
        }
    
        private void registerCallBackUrl(String appKey,String appSecret,String corpId) throws ApiException {
            //1. 先查询这个钉钉法体是否已经注册了回调地址
            //2. 注册了则看注册地址跟这次是否一致,不一致则删除后再重新注册
            String callbackUrl = dingTalkConfig.getCallbackUrl() + corpId.substring(0, 15) + StringUtil.md5(appKey + appSecret).substring(0,21);
            OapiCallBackGetCallBackResponse callBackGetCallBackResponse = getCallBackGetCallBackResponse(appKey, appSecret);
    
            log.info("查询事件回调接口:【{}】", JsonUtil.defaultMapper().toJson(callBackGetCallBackResponse));
            if (null == callBackGetCallBackResponse || !callbackUrl.equals(callBackGetCallBackResponse.getUrl())) {
    
                // 先删除企业已有的回调
                DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/call_back/delete_call_back");
                OapiCallBackDeleteCallBackRequest request = new OapiCallBackDeleteCallBackRequest();
                request.setHttpMethod("GET");
    
                //获取accessToken
                String token = getToken(appKey, appSecret);
                client.execute(request, token);
    
                // 重新为企业注册回调
                client = new DefaultDingTalkClient("https://oapi.dingtalk.com/call_back/register_call_back");
                OapiCallBackRegisterCallBackRequest registerRequest = new OapiCallBackRegisterCallBackRequest();
                registerRequest.setUrl(callbackUrl);
                registerRequest.setAesKey(dingTalkConfig.getEncodingAesKey());
                registerRequest.setToken(dingTalkConfig.getToken());
                registerRequest.setCallBackTag(Arrays.asList("bpms_instance_change", "bpms_task_change"));
                OapiCallBackRegisterCallBackResponse registerResponse = client.execute(registerRequest, token);
                if (registerResponse.isSuccess()) {
                    System.out.println(corpId + " : 回调注册成功了!!!");
                    log.info("项目启动完成,钉钉回调注册成功了!,CorpId: 【{}】 回调地址为:【{}】", corpId, callbackUrl);
                } else {
                    System.out.println(corpId + " : 回调注册失败了,Error: " + JSON.toJSONString(registerResponse));
                }
            }
        }
    
        /**
        * 获取账号的accessToken
        *
        * @return
        * @throws ApiException
        */
        public String getToken(String appKey, String appSecret) {
    
            // accessToken 两个小时过期,期间获取钉钉仍是返回相同的值
            // 所以先存储到redis中,之后可直接从redis中获取
            String key = RedisConstants.DINGTALK_ACCESSTOKEN + appKey;
            String accessToken = (String) redisUtil.get(key);
            if (StringUtil.isNotEmpty(accessToken)) {
                return accessToken;
            }
    
            DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
            OapiGettokenRequest req = new OapiGettokenRequest();
            req.setAppkey(appKey);
            req.setAppsecret(appSecret);
            req.setHttpMethod("GET");
            OapiGettokenResponse rsp = null;
            try {
                rsp = client.execute(req);
            } catch (ApiException e) {
                log.error("DingTalk: getAccessToken: error: ", e);
                throw new SystemException("获取钉钉AccessToken失败,请联系技术人员处理", ResultEnum.FAIL.getValue());
            }
            accessToken = rsp.getAccessToken();
            redisUtil.set(key, accessToken, 6900);
            return accessToken;
        }
    }
    
    
    
    • 回调信息处理
    @RestController
    @RequestMapping("/processCallBack")
    @Slf4j
    public class CallBackController {
    
        @Resource
        private ISimbaCompanyService simbaCompanyService;
    
        @Resource
        private DingTalkConfig dingTalkConfig;
    
        @PostMapping("/callback/{corpId}")
        @ResponseBody
        public Map<String, String> callback(@RequestParam(value = "signature", required = false) String signature,
                                            @RequestParam(value = "timestamp", required = false) String timestamp,
                                            @RequestParam(value = "nonce", required = false) String nonce,
                                            @RequestBody(required = false) JSONObject json,
                                            @PathVariable("corpId") String corpId
        ) {
            DingTalkCallBackQuery query = new DingTalkCallBackQuery();
            query.setSignature(signature);
            query.setTimestamp(timestamp);
            query.setNonce(nonce);
            query.setJson(json);
            log.info("进入dingTalk回调流程,参数:【{}】", query);
            try {
                //查询组织对应的钉钉配置信息
                DynamicDataSourceContextHolder.setDataSource(DataSourceType.TN, "");
                SimbaCompany simbaCompany = simbaCompanyService.queryByStartCorpId(corpId.substring(0, 15));
    
                DingTalkEncryptor dingTalkEncryptor = new DingTalkEncryptor(dingTalkConfig.getToken(), dingTalkConfig.getEncodingAesKey(), simbaCompany.getCorpId());
    
                //先对获取的回调信息的加密数据进行解密处理
                String plainText = dingTalkEncryptor.getDecryptMsg(signature, timestamp, nonce, json.getString("encrypt"));
                handleDingTalkCallBack(plainText, simbaCompany.getAppKey(), simbaCompany.getAppSecret());
    
                return dingTalkEncryptor.getEncryptedMap("success", System.currentTimeMillis(), Utils.getRandomStr(8));
            } catch (Exception e) {
                //失败的情况,应用的开发者应该通过告警感知,并干预修复
                log.error("process callback failed!" + query, e);
                return null;
            }
        }
    
    
        private void handleDingTalkCallBack(String plainText, String appKey, String appSecret) {
            try {
                JSONObject obj = JSON.parseObject(plainText);
                log.info("dingTalk回调解密后参数:【{}】", obj);
    
                String eventType = obj.getString("EventType");
                if (!"check_url".equals(eventType)) {
                    // 1. 在提交审批成功后将返回的processInstanceId和数据源存储到redis
                    // 2. 回调处理时查询redis,然后切换到对应的数据源进行后续处理
    
                    //切换数据源 如果查不到,直接返回,不做后续处理(防止他人直接从钉钉提交审批,不走ERP流程)
                    String organization = (String) redisUtil.get(RedisConstants.DINGTALK_INSTANCEID + obj.getString("processInstanceId"));
                    if (organization == null){
                        return;
                    }
    
                    //根据staffId查询审批人的信息,根据钉钉手机号再查询ERP系统中的用户
                    OapiUserGetResponse operator = dingTalkUtils.getUser(obj.getString("staffId"), appKey, appSecret);
                    String ddMobile = operator.getMobile();
                    SimbaUser simbaUser = simbaUserService.getByDdMobile(ddMobile);
                    if (simbaUser == null) {
                        simbaUser = new SimbaUser(0, "system");
                    }
    
                    //根据解密信息中的回调数据类型做不同的业务处理
                    CallBackHandler handlerInstance = dingTalkCallBackManager.getHandlerInstance(eventType);
    
                    DynamicDataSourceContextHolder.setDataSource(DataSourceType.TN, organization);
                    DynamicDataSourceContextHolder.setSystemHolder(DataSourceType.TN.getCode());
                    DynamicDataSourceContextHolder.setOrgnizationHolder(organization);
    
                    handlerInstance.handleCallBackLogic(obj, simbaUser);
                }
    
            } catch (Exception e) {
                log.error("钉钉回调出错,参数:【{}】", plainText, e);
    
                //处理异常推送信息到钉钉群
                dingTalkUtils.sendERPMsg("钉钉回调失败:Env: 【" + env + "】 AppKey: 【" + appKey + "】 AppSecret: 【" + appSecret + "】 Msg: 【" + plainText + "】 Exception: 【" + e + "】");
            }
        }
    }
    
    • 回调事件处理器
    @Component
    public class DingTalkCallBackManager {
    
        /**
        * 获取具体的处理器
        *
        * @param eventType
        * @return
        */
        public CallBackHandler getHandlerInstance(String eventType) {
    
            DingTalkCallBackEventEnum of = DingTalkCallBackEventEnum.of(eventType);
    
            return (CallBackHandler) SpringContextUtils.getBean(of.getHandlerClass());
        }
    
        // public enum DingTalkCallBackEventEnum {
        //     PROCESS_TASK("审批任务回调", "bpms_task_change", "ProcessTaskChangeHandler"),
        //     PROCESS_INSTANCE("审批实例回调", "bpms_instance_change", "ProcessInstanceChangeHandler"),
        //     OTHER("其他处理器", "OtherHandler", "OtherHandler"),
        //     ;
        // }
    
    }
    
    //再继承处理器写详细的业务代码就行了
    public interface CallBackHandler {
    
        /**
        * 处理回调逻辑
        * @param callbackInfo
        */
        void handleCallBackLogic(JSONObject callbackInfo, SimbaUser simbaUser);
    }
    
    @Component
    @Slf4j
    public class SpringContextUtils implements ApplicationContextAware {
        private static ApplicationContext applicationContext = null;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            if (SpringContextUtils.applicationContext == null) {
                SpringContextUtils.applicationContext = applicationContext;
            }
        }
    
        /**
        * @apiNote 获取applicationContext
        */
        public static ApplicationContext getApplicationContext() {
            return applicationContext;
        }
    
        /**
        * @apiNote 通过name获取 Bean.
        */
        public static Object getBean(String name) {
            return getApplicationContext().getBean(name);
        }
    
        /**
        * @apiNote 通过class获取Bean.
        */
        public static <T> T getBean(Class<T> clazz) {
            return getApplicationContext().getBean(clazz);
        }
    
        /**
        * @apiNote 通过name, 以及Clazz返回指定的Bean
        */
        public static <T> T getBean(String name, Class<T> clazz) {
            return getApplicationContext().getBean(name, clazz);
        }
    
        /**
        * 获取当前主机的hostname
        * @return
        */
        public static String getHostName(){
            try {
                InetAddress ia = InetAddress.getLocalHost();
                return ia.getHostName();
    
            }catch (Exception e){
                log.info("获取当前主机名失败【{}】",e.getMessage());
            }
            return "";
        }
    
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值