超详细!!!飞书对接金蝶星空旗舰版(公有云、天梯部署),订阅审批事件
飞书审批对接金蝶云星空旗舰版
一、前言
1.1 需求案例
飞书出差申请流程审批通过之后,传到金蝶云星空旗舰版,生成出差申请单
1.2 实现步骤
- 搭建项目工程(从gitlab仓库拉取)
- 编写api接口,保存出差申请单(先用假数据写死),并在本地测试通过
- 调用飞书接口,获取审批表单信息
- 解析审批表单信息并替换掉假数据,再次测试通过
- 通过天梯部署到沙箱环境
- 发布api接口,配置到飞书事件回调地址
- 沙箱测试
1.3 提前准备信息
- 飞书管理员账号(应用权限、审批管理权限、API在线调试)
- 金蝶星空旗舰版账号(第三方应用、自定义API、项目构建)
- 金蝶云天梯账号(提单部署、查看日志)
- 金蝶协同开发平台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以后,再去调用接口获取实例详情。
六、日志查看
金蝶云天梯-后台应用日志
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)