一、前言

1.1 需求案例

飞书出差申请流程审批通过之后,传到金蝶云星空旗舰版,生成出差申请单

1.2 实现步骤

  1. 搭建项目工程(从gitlab仓库拉取)
  2. 编写api接口,保存出差申请单(先用假数据写死),并在本地测试通过
  3. 调用飞书接口,获取审批表单信息
  4. 解析审批表单信息并替换掉假数据,再次测试通过
  5. 通过天梯部署到沙箱环境
  6. 发布api接口,配置到飞书事件回调地址
  7. 沙箱测试

1.3 提前准备信息

  1. 飞书管理员账号(应用权限、审批管理权限、API在线调试)
  2. 金蝶星空旗舰版账号(第三方应用、自定义API、项目构建)
  3. 金蝶云天梯账号(提单部署、查看日志)
  4. 金蝶协同开发平台gitlab账号(推送jar包到仓库)

二、飞书

本节目的主要是为了拿到飞书审批表单信息

2.1 创建企业自建应用

登录飞书开发者后台,创建企业自建应用
在这里插入图片描述

2.2 开通应用权限

打开应用,在权限管理中开通以下权限,开通后发布版本才能生效。
在这里插入图片描述

2.3 获取流程定义code

2.3.1 进入飞书审批管理后台
在这里插入图片描述
2.3.2 打开出差申请单
可以看到地址上的definitionCode,即流程定义code
在这里插入图片描述

2.4 API调试

2.4.1 打开API调试台 审批-原生审批实例-批量获取审批实例ID

在这里插入图片描述
approval_code填入1.3中的流程定义code,填入开始时间和结束时间的13位时间戳,点击开始调试
可以看到返回的instance_code_list,即实例code列表(如果没有数据,自己去飞书上走一笔出差申请流程)

2.4.2 获取获取单个审批实例详情
审批-原生审批实例-获取获取单个审批实例详情
form即为要保存的单据信息
在这里插入图片描述
form结构分析:
form可以解析成JSON数组,可以根据其中的name获取值,name为审批表单设计中标题
在这里插入图片描述

三、编写插件

3.1 进入协同开发平台

在这里插入图片描述

3.2 复制仓库路径

在这里插入图片描述

3.3 IDEA新建协同开发项目工程(如果没有该选项,请自行安装苍穹开发助手

在这里插入图片描述

3.4 填写仓库地址

在这里插入图片描述## 3.5 填写工程信息
在这里插入图片描述

3.6 生成项目工程

在这里插入图片描述

3.7 编写自定义api接口(请自行提取代码)

先别急,在后面5.7会进行代码说明
Controller

@ApiController(value = "fs",desc = "飞书实例转换成金蝶单据")
public class FsInstanceTransferController implements Serializable {

    private static Log logger = LogFactory.getLog(FsInstanceTransferController.class);
    @ApiPostMapping(value = "instance")
    public CustomApiResult<JSONObject> transfer(@Valid @ApiParam(value = "请求参数") String challenge, @Valid @ApiParam(value = "请求参数") String type, @Valid @ApiParam(value = "请求参数") JSONObject event) throws IOException, ParseException {
        logger.info("进入FsInstanceTransferController");
        CustomApiResult<JSONObject> apiResult = new CustomApiResult<>();
        JSONObject jsonObj = new JSONObject();
        if ("url_verification".equals(type))
        {
            //认证
            jsonObj.put("challenge", challenge);
            apiResult.setStatus(true);
            apiResult.setData(jsonObj);
        }

        if ("event_callback".equals(type))
        {
            //回调
            if (event!=null)
            {
                //{"approval_code":"3D826261-7CD5-4B81-8D5B-D864E4011175","tenant_key":"14d747c5d7d5575f","instance_operate_time":"1739961099652","instance_code":"A7D3D964-5173-4976-B7F4-9AF852A903CD","type":"approval_instance","app_id":"cli_a73873607efa900b","uuid":"fe9d9e50","operate_time":"1739961099652","status":"APPROVED"}
                //审批定义 Code
                String approval_code = event.getString("approval_code");
                //审批实例 Code
                String instance_code = event.getString("instance_code");
                logger.info("飞书对应审批信息approval_code=" + approval_code + ",instance_code=" + instance_code);

                //查询审批实例
                String token = FeishuUtils.getToken();
                JSONObject instanceDetail = FeishuUtils.getInstanceDetail(token, instance_code);
                logger.info("飞书instanceDetail:"+instanceDetail.toJSONString());
                JSONObject data = instanceDetail.getJSONObject("data");
                //流程审批通过时执行逻辑
                if("APPROVED".equals(data.getString("status"))){
                    JSONArray form = data.getJSONArray("form");
                    if(AppflgConstant.Trip_Approval_Code.equals(data.getString("approval_code"))){
                        logger.info("开始保存出差申请单");
                        //处理业务逻辑
                    }
                }


            }
        }
        return apiResult;
    }
}

飞书工具类

public class FeishuUtils {
    // 设置请求超时10分钟 根据业务调整
    private static Integer CONNECTION_TIMEOUT = 600 * 1000;
    // 设置等待数据超时时间10分钟 根据业务调整
    private static Integer SO_TIMEOUT = 600 * 1000;
    /**
     * 飞书认证
     * @return
     */
    public static String getToken() {
        try {
            Map<String, String> headersMap = new HashMap<>();
            headersMap.put("Content-Type", "application/json;charset=utf-8");

            Map<String, Object> bodyMap = new HashMap<>();
            bodyMap.put("app_id", AppflgConstant.FS_APP_ID);
            bodyMap.put("app_secret", AppflgConstant.FS_APP_SECRET);
            String bodyJson = JSONObject.toJSONString(bodyMap);

            String result = HttpClientUtils.postjson(AppflgConstant.FSTokenUrl, headersMap,
                    bodyJson, CONNECTION_TIMEOUT, SO_TIMEOUT);
            JSONObject data = JSONObject.parseObject(result);
            String code = data.getString("code");
            String token = "";

            if("0".equals(code)){
                token = data.getString("tenant_access_token");
            }else{
                String msg = data.getString("msg");
                throw new KDBizException("获取【飞书token】失败,原因:" + msg);
            }
            return token;
        } catch (IOException e) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            throw new KDBizException("获取【飞书token】接口错误日志:" + sw.toString());
        }
    }

    /**
     * 获取单个实例详情
     * @return
     */
    public static JSONObject getInstanceDetail(String token,String instance_code) {
        String url = AppflgConstant.FSInstanceDetailUrl + instance_code;
        return sendGet(url, token);
    }

    public static JSONObject sendGet(String url, String token) {
        JSONObject obj = null;

        OkHttpClient client = new OkHttpClient().newBuilder()
                .build();
        Request request = new Request.Builder()
                .url(url)
                .get()
                .addHeader("Authorization", "Bearer " + token)
                .addHeader("text/plain","charset=utf-8")
                //.addHeader("Host", "<calculated when request is sent>")
                .build();

        JSONObject result = null;
        try {
            Response response = client.newCall(request).execute();
            String res = response.body().string();
            result = JSONObject.parseObject(res);
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;

    }

    public static JSONObject sendPost(String json, String URL, String token) {
        JSONObject resultObj = new JSONObject();
        CloseableHttpClient client = HttpClients.createDefault();
        HttpPost post = new HttpPost(URL);
        post.setHeader("Content-Type", "application/json");
        if (token != null && !"".equals(token)) {
            //String auth = "cosmic:scc123456+";
            //String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
            post.setHeader("Authorization", "Bearer " + token);
        }

        String result;
        try {
            StringEntity s = new StringEntity(json, "utf-8");
            s.setContentType(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
            post.setEntity(s);
            //发送请求
            HttpResponse httpResponse = client.execute(post);
            //获取响应输入流
            InputStream inStream = httpResponse.getEntity().getContent();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inStream, "utf-8"));
            StringBuilder strber = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                strber.append(line + "\n");
            }
            inStream.close();
            result = strber.toString();
            JSONObject jsonObject = JSON.parseObject(result);
            if (jsonObject != null) {
                String s1 = jsonObject.toJSONString();
                int code = jsonObject.getInteger("code");
                String msg = jsonObject.getString("msg");
                if (code==0) {
                    resultObj.put("success", true);
                    resultObj.put("msg", msg);
                    return jsonObject;
                }else{
                    resultObj.put("success", false);
                    resultObj.put("msg", msg);
                }
            }
        } catch (Exception e) {
            String errorMessage = e.getMessage();
            resultObj.put("success", false);
            resultObj.put("errorMessage", errorMessage);
        }
        return resultObj;
    }




}

常量类

public class AppflgConstant {
	public static final String FS_APP_ID = "飞书应用ID";
	public static final String FS_APP_SECRET = "飞书应用密钥";
	public static final String FSTokenUrl = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
	public static final String FSInstanceDetailUrl = "https://open.feishu.cn/open-apis/approval/v4/instances/";
	public static final String Trip_Approval_Code = "出差申请单流程定义code";

}

四、代码部署

4.1 右键工程,git进行提交并推送到仓库

在这里插入图片描述
注意:第一次提交需要将gradle配置文件一起提交,否则编译时会无法加载到依赖包,构建时会报错。
在这里插入图片描述

如果没有git选项,需要下载git并配置
在这里插入图片描述

4.2 在线构建

登录协同开发平台(或者星空旗舰版-协同开发云,区别是协同开发平台可以查看详细的构建、扫描日志),选中项目,点击在线构建。
在这里插入图片描述
构建:系统会检查代码有无异常以及依赖是否正确,若无误,则会自动生成元数据包以及jar包
质量扫描:系统会检查代码是否会出现抛出异常的情况,比如空指针未处理,或者文件流未关闭,并对代码质量进行评级,A、B可以直接审核通过,C级或以上需要规范代码或者在制品列表里面手动进行审核。

4.3 发布制品

质量扫描通过之后,可以在制品列表里面选中制品,并推送到天梯
在这里插入图片描述

4.4 天梯提单部署

登录金蝶云天梯-协同二开补丁提单
在这里插入图片描述
说明:
1.当制品推送天梯并且审核通过,才能选到补丁
2.应用类型选择扩展应用
3.计划时间看情况选择
4.部署策略分为常规部署和热部署,当制品包含jar包时,常规部署是每天12:00、18:00、21:00 自动重启,热部署是立即生效,可以直接选择热部署。
5.部署到生产环境每天早上五点钟以后不定时重启

提单提交后,可以在二开补丁任务查看任务状态
在这里插入图片描述

五、 自定义API配置&飞书回调配置

5.1 发布自定义API

登录金蝶云星空旗舰版,进入开放服务云-openAPI-API开发
新增自定义API
在这里插入图片描述
在这里插入图片描述
类名填写3.1.7中的Controller,勾选匿名访问出参仅返回Data域,会自动生成请求地址

5.2 API测试

点击API测试,请求参数输入{"type":"url_verification","challenge":"123abcpwosaj"} ,当返回{"challenge":"123abcpwosaj"}说明测试成功
在这里插入图片描述

5.3 新建第三方应用

虽然上一步已经测试成功了,但这只是在内部调用,第三方无法调用。第三方调用必须在请求头上拼接x-acgw-identity
在这里插入图片描述
自建API授权绑定刚新建的自定义API
在这里插入图片描述
复制x-acgw-identity
在这里插入图片描述
到这一步,已经拿到了完整的请求地址:
https://ip/kapi/v2/g881/iscb/instance?x-acgw-identity=*****
在postman测试
在这里插入图片描述

5.4 飞书配置回调地址

在这里插入图片描述
订阅方式说明:简略说明,详细可以去看飞书官方文档
长连接:接入飞书的sdk,工程启动后,会和飞书建立全双工通道,方便在本地进行调试;
将事件发送至开发者服务器:需要提供请求地址,请求地址必须是公网,并且能在一秒内响应指定信息(格式参见5.2)

本文使用的是第二种方式
如果请求地址保存不成功,请在postman测试接口是否正常,并能返回指定格式的json,如果正常,再去检查服务器防火墙。如果出参格式不正确,请检查5.1是否勾选出参仅返回Data域

5.5 添加事件

点击添加事件按钮,根据实际业务需求添加事件,本案例需要监听审批实例审核,所以选择审批实例状态变更事件
在这里插入图片描述

5.6 订阅审批事件

添加审批实例状态变更事件后,还需要指定订阅哪个审批,在API调试台执行订阅审批事件
在这里插入图片描述

5.7 代码说明

到了这一步,说明对整个流程已经熟悉了,更方便理解代码
首先,在5.5保存请求地址时,飞书会传入参数{"type":"url_verification","challenge":"123abcpwosaj"} ,验证请求地址是否会返回{"challenge":"123abcpwosaj"}(challenge可以是任意字符串)
当监听了出差审批并且审批实例状态变更后,飞书会传入请求参数{"type":"event_callback","event":{"instance_code":"1AA6C533-3A4C-48B6-A82A-16432EA7FEE5"}} ,拿到实例code以后,再去调用接口获取实例详情。

六、日志查看

金蝶云天梯-后台应用日志
在这里插入图片描述

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐