APL函数实战

319 阅读7分钟

场景1(项目券申领、项目券、项目券转让)

场景描述:

  1. 提交项目券申领的申请,审批通过后,自动新建项目券,项目券的信息来源于项目券申领

  2. 提交项目券转让的申请,审批通过后,自动新建项目券,项目券的信息来源于项目券转让、先前项目券

  3. 项目券转让时,涉及一些项目券字段中一些金额的计算, 有项目券金额已使用金额剩余可用金额已转让金额

    其中有业务逻辑:

    1. 转让前的项目券项目券金额不会变动

    2. 转让前的项目券已转让金额 = 先前已转让金额 + 本次转让金额

    3. 转让前的项目券剩余可用金额 = 项目券金额 - 已转让金额 - 已使用金额

    4. 转让后新生成的项目券项目券金额 = 本次转让金额

实现:

  1. 由于项目券申领的审批走的不是审批流(而是外部接口),所以函数无法挂载在审批流通过后动作上。沟通了解到LIMS审批通过后LIMS审批结果字段会变更,所以可以将函数挂载在工作流上,监听项目券申领LIMS审批结果字段的变更。

    代码:

    // 获取触发流程的对象的id与apiname
    String objectId = context.data["_id"]
    String objectApiName = context.data["object_describe_api_name"]
    
    // 查询项目券申领对象的数据(负责人、申领类型、申领项目券金额、申领客户、申领单位)
    String sql = "select owner, couponType__c, couponApplicationAmount__c, couponApplicationContact__c, couponApplicationAccount__c from object_Ur0mD__c where _id = '$objectId'";
    SelectAttribute att = SelectAttribute.builder()
      .needCount(true)
      .needInvalid(true)
      .build();
    def(Boolean error, Object result, String message) = Fx.object.select(sql, att);
    if (error) {
      log.info("error:" + error)
      log.info("message:" + message)
    }
    QueryResult queryResult = result as QueryResult
    List dataList = (queryResult.dataList == null) ? ([]) : queryResult.dataList as List // 查询结果
    
    // 对查询结果进行处理
    if( dataList.size() == 0 ){
      log.info('查询不到该项目券申领对象')
      return
    }
    Map ticketMap = [:]
    Map dataMap = dataList[0]
    List ownerList = (dataMap["owner"] == null) ? ([]) : dataMap["owner"] as List
    ticketMap.put("owner", ownerList)
    String owner_ = (ownerList.size() == 0) ? ('') : ownerList[0] // 负责人
    String couponType = (dataMap["couponType__c"] == null) ? ('') : dataMap["couponType__c"] as String // 申领类型
    ticketMap.put("couponType__c", couponType)
    BigDecimal couponApplicationAmount = (dataMap["couponApplicationAmount__c"] == null) ? (0) : dataMap["couponApplicationAmount__c"] as BigDecimal // 申领项目券金额
    ticketMap.put("couponAmount__c", couponApplicationAmount)
    String couponApplicationContact = (dataMap["couponApplicationContact__c"] == null) ? ('') : dataMap["couponApplicationContact__c"] as String // 申领客户
    ticketMap.put("couponContactBelongs__c", couponApplicationContact)
    String couponApplicationAccount = (dataMap["couponApplicationAccount__c"] == null) ? ('') : dataMap["couponApplicationAccount__c"] as String // 申领单位
    ticketMap.put("couponAccountBelongs__c", couponApplicationAccount)
    ticketMap.put("couponState__c", "1") // 项目券状态
    
    // 项目券创建,已转让金额默认给个0
    ticketMap.put("couponTransferAmount__c", 0)
    log.info(ticketMap)
    
    // 创建项目券对象
    ActionAttribute attribute = ActionAttribute.build {
      triggerApprovalFlow = false
      triggerWorkflow = false
      skipFunctionAction = true
      specifyCreatedBy = true
      specifyTime = true
      duplicateSearch = false
    }
    def(Boolean error1,Map data1,String errorMessage1) = Fx.object.create("object_cOCdW__c", ticketMap, null,attribute)
    Map map = data1.data as Map
    String ticketId = map["_id"] // 刚刚创建的项目券的id
    
    
    // 项目券申领中回填刚刚创建的项目券的编号
    def (Boolean error2, Map data2, String errorMessage2) =  Fx.object.update("object_Ur0mD__c", objectId, ["couponID__c": ticketId], false)
    if (error2) {
      log.info("error2:" + errorMessage2 )
    }
    log.info(data2)
    
    
    
  2. 项目券转让的申请走的是审批流,所以可以在审批通过后动作挂载函数

    代码:

    
    // 封装项目券对象的各字段内容
    Map ticketMap = [:]
    
    // 获取项目券转让申请对象字段信息
    String objectId = (context.data["_id"] == null) ? ("") : context.data["_id"] as String
    String name = (context.data["name"] == null) ? ("") : context.data["name"] as String // 项目券转让申请的编号
    String objectApiName = (context.data["object_describe_api_name"] == null) ? ("") : context.data["object_describe_api_name"] as String
    String couponId = (context.data["couponId__c"] == null) ? ("") : context.data["couponId__c"] as String // 项目券编号(查找关联字段)
    List newSales = (context.data["newSales__c"] == null) ? ([]) : context.data["newSales__c"] as List // 转入销售 -> 对应新建项目券的负责人
    ticketMap.put("owner", newSales)
    String couponNewContact = (context.data["couponNewContact__c"] == null) ? ("") : context.data["couponNewContact__c"] as String // 转入客户 -> 对应新建项目券的所属客户联系人
    ticketMap.put("couponContactBelongs__c", couponNewContact)
    String couponNewAccount = (context.data["couponNewAccount__c"] == null) ? ("") : context.data["couponNewAccount__c"] as String // 转入单位 -> 对应新建项目券的所属单位
    ticketMap.put("couponAccountBelongs__c", couponNewAccount)
    BigDecimal couponTransferAmount = (context.data["couponTransferAmount__c"] == null) ? (0.00) : context.data["couponTransferAmount__c"] as BigDecimal // 转让金额 -> 对应新建项目券的项目券金额
    ticketMap.put("couponAmount__c", couponTransferAmount)
    ticketMap.put("couponTransferAmount__c", 0.0) // 新建项目券的已转让金额默认为0
    
    String couponTransferReasons = (context.data.couponTransferReasons__c == null) ? ("") : ("经转让生成的项目券,项目券转让申请单号: " + name + ", 转让原因: " + context.data.couponTransferReasons__c) // 转让原因 -> 对应新建项目券的备注
    ticketMap.put("remark__c", couponTransferReasons)
    
    // 查询关联的项目券的类型by 项目券编号
    String sql = "select couponType__c from object_cOCdW__c where _id = '${couponId}' "
    SelectAttribute att = SelectAttribute.builder()
    .needCount(true)
    .needInvalid(true)
    .build();
    def(Boolean error, Object result, String message) = Fx.object.select(sql, att);
    if (error) {
    log.info("error:" + error)
    }
    QueryResult queryResult = result as QueryResult
    List dataList = queryResult.dataList as List
    Map dataMap = dataList[0]
    String couponType = (dataMap["couponType__c"] == null) ? ("") : dataMap["couponType__c"] as String
    ticketMap.put("couponType__c", couponType) // 项目券类型放入ticketMap
    log.info(ticketMap)
    
    // 创建项目券对象
    ActionAttribute attribute = ActionAttribute.build {
    triggerApprovalFlow = false
    triggerWorkflow = false
    skipFunctionAction = true
    specifyCreatedBy = true
    specifyTime = true
    duplicateSearch = false
    }
    def(Boolean error1,Map data1,String errorMessage1) = Fx.object.create("object_cOCdW__c", ticketMap, null,attribute)
    

image.png

这个需求可以通过审批后动作实现:

image.png

不需要写函数

image.png

这个需求可以通过计算字段实现:

image.png

image.png

也不需要写函数

  1. 关于项目券金额的整个流程就是: 新建项目券A时有一个项目券金额,然后转让项目券的时候会利用转让的金额新创建一个项目券B,那么项目券B的金额就是转让金额。原来的项目券A的已转让金额会根据审批通过后动作的字段变更来更新,原来的项目券A的剩余金额为根据计算字段来自动更新。

场景2(集成流同步后函数)

场景描述

  1. 通过集成流无法实现将CRM对象中的附件字段的附件推送到K3C相应对象的附件字段中

解决方案

方案1

在同步后函数中实现将附件由CRM上传到云星空

方案2

CRM对象中增加一个标识字段,当集成流成功把数据从CRM推送到K3C后,CRM对象中的标识字段变为"成功",由此触发工作流,工作流中实现自定义函数,将附件由CRM上传到云星空

代码

不管是方案1还是方案2,都需要编写附件由CRM上传到云星空的代码

思路:

  1. 首先利用postman + python对云星空进行了接口测试 ✔
  2. 在纷享APL代码中利用Fx.file相关api,根据文件npath,可以获取到文件的字节数组(仅支持几mb小文件)或者输入流(可支持20mb左右大文件) ✔
  3. 再将字节数组/输入流进行base64编码(云星空上传文件接口只接收文件base64编码后的字符串) ✔
  4. 调用云星空web接口进行附件上传 ✔

具体实现

  1. 接口测试代码

    1. 登录

    2. 上传附件

  2. 将登录/上传附件封装成APL类,方便在同步后函数中调用

        class UploadFileToK3C {
    
       private String loginUrl = 'http://220.176.183.42:8081/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc'
       private String uploadUrl = 'http://220.176.183.42:8081/K3Cloud/Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.AttachmentUpLoad.common.kdsvc'
    
       private String cookies = '' // 上传文件需要,由登录获取
    
       // 登录
       void login() {
         def headers = ["Content-Type": 'application/json']
         def data = [
             "parameters": [
             "数据中心id",  
             "云星空账号",  
             "云星空密码", 
             2052  
           ]
         ]
    
         def (Boolean error,HttpResult result,String errorMessage) = Fx.http.post(this.loginUrl, headers, data, 2000, true, 2, false)
         if( error || result.statusCode != 200 ){
           log.info('登录失败: ' + errorMessage)
         } else {
           log.info('登录成功')
           // 取出cookie
           Map headerMap = result['headers'] as Map
           this.cookies =headerMap['Cookies'] as String
           log.info('cookies为: ' + this.cookies)
         }
       }
    
       /**
        * 上传文件到云星空
        * String fileName: 要上传的文件的名字
        * String fileNpath: 要上传的文件的npath
        * String formId: 表单id
        * Boolean isLast: 是否最后一次上传
        * String interId: 单据内码
        * String billNo: 单据编号
        **/
       void singleUpload(String fileName, String fileNpath, String formId, Boolean isLast, String interId, String billNo) {
         // 校验登录
         if( this.cookies == null ){
           login()
         }
    
         // 根据npath从纷享文件服务器获取文件输入流,并对输入流进行base64编码
         def(Boolean err, Object fileData, String msg) = Fx.file.downloadStream(fileNpath)
         if(err) {
           log.info('获取文件输入流失败: ' + msg)
         }
         InputStream inputStream = fileData['inputStream'] as InputStream
         // 使用自定义jar包: 输入流 -> base64字符串,为了满足云星空上传附件接口条件
         def strBase64 = fx.custom.apl.jar.customUtils.InputStreamToBase64.transfer(inputStream) 
    
         def headers = [
           "Content-Type": "application/json",
           "Cookie": this.cookies
         ]
    
         def data = [
           "data": [
             "FileName": fileName, 
             "FormId": formId,
             "IsLast": isLast,
             "InterId": interId,
             "BillNO": billNo,
             "SendByte": strBase64
           ]
         ]
    
         def (Boolean error,HttpResult result,String errorMessage) = Fx.http.post(this.uploadUrl, headers, data, 120000, false, 3, true)
         if( error || result.statusCode != 200 ){
           log.info('上传附件到云星空失败')
           log.info('错误信息为: ' + errorMessage)
           log.info('错误结果为:' + result)
         } else {
           log.info('上传附件到云星空成功')
           log.info('成功结果为:' + result)
         }
       }
    
       //debug 时候的入口方法
       static void main(String[] args) {
           UploadFileToK3C testObj = Fx.klass.newInstance('UploadFileToK3C') as UploadFileToK3C
           testObj.login()
           testObj.singleUpload('呜呜呜呜2.txt', 'N_202307_19_37d3eea0cf394f6b85ea546c74dbba4f.txt', 'CRM_Contract', true, '100008', 'XSHT000002')
       }
    
    }
    
  3. 上传附件时依赖了一个Jar包,实现将输入流转为base64字符串, 代码如下:

      package fx.custom.apl.jar.customUtils;
      import java.io.ByteArrayOutputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.util.Base64;
      public class InputStreamToBase64 {
    
          /**
           * 输入流转base64字符串
           * @param is 输入流
           * @return base64字符串
           * @throws Exception 输入流关闭异常
           */
          public static String transfer(InputStream is) throws Exception {
              byte[] data = null;
              try {
                  ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
                  byte[] buff = new byte[100];
                  int rc = 0;
                  while ((rc = is.read(buff, 0, 100)) > 0) {
                      swapStream.write(buff, 0, rc);
                  }
                  data = swapStream.toByteArray();
              } catch (IOException e) {
                  e.printStackTrace();
              } finally {
                  if (is != null) {
                      try {
                          is.close();
                      } catch (IOException e) {
                          throw new Exception("输入流关闭异常");
                      }
                  }
              }
    
              return Base64.getEncoder().encodeToString(data);
          }
      }
    
    
    1. 同步后函数
            /**
         * 同步后函数,数据同步不会受到影响
         */
        @Override
        IntegrationStreamSync.AfterSyncResult executeAfterSync(FunctionContext context, IntegrationStreamSync.AfterSyncArg arg) {
            String sourceObjectApiName = 'SaleContractObj' // 源系统对象apiName
            String destObjectApiName = "CRM_Contract" // 目标系统对象apiName
            String sourceDataId = arg.getSourceDataId() // 源系统数据id,只有crm->erp方向有这个字段
            CompleteDataWriteMqData writeData = arg.getCompleteDataWriteResult() // 同步目标数据结果
            CompleteDataWriteMqData.WriteResult writeResult = writeData.getWriteResult() // 同步主数据结果
            SimpleSyncData simpleSyncData = writeResult.getSimpleSyncData() // 同步数据简略信息
            String destDataId = simpleSyncData.getDestDataId() // 目标系统数据id (销售合同的单据内码?)
            String destDataName = simpleSyncData.getDestDataName() // 目标系统数据主属性 (销售合同的单据编号?)
    
            Integer destEventType = arg.getDestEventType() // 目标系统执行动作 1、新增 2、修改 3、作废
            if( destEventType == 1 || destEventType == 2 ){ 
              int code = writeData.getErrCode() // 同步目标数据结果, 0为正常,其他为失败接口返回的错误码
              String msg = writeData.getErrMsg() // 失败接口返回的错误信息
              if( code == 0 ){
                // CRM里查询销售合同附件字段内容
                String sql = "select field_f2csm__c from $sourceObjectApiName where _id = '$sourceDataId'"
                SelectAttribute att = SelectAttribute.builder()
                  .needCount(true)
                  .needInvalid(true)
                  .build()
                def(Boolean error, Object result, String message) = Fx.object.select(sql, att)
                if (error) {
                  log.info("CRM里查询销售合同附件字段内容出错,error: " + error)
                  log.info("CRM里查询销售合同附件字段内容出错,message: " + message)
                }
                QueryResult queryResult = result as QueryResult
                List dataList = (queryResult.dataList == null) ? ([]) : queryResult.dataList as List
                if( dataList.size() == 0 ){
                  def resultRet = new IntegrationStreamSync.AfterSyncResult()
                  resultRet.setCode("1") // 返回结果状态 0为正常,其他都是失败
                  resultRet.setMessage("同步后函数执行失败, 提取附件字段内容出错") // 返回的错误信息
                  return resultRet
                }
                Map dataMap = dataList[0]
                List fileList = (dataMap['field_f2csm__c'] == null) ? ([]) : dataMap['field_f2csm__c'] as List // 附件列表
    
                // 创建APL类的对象
                UploadFileToK3C uploadFileToK3C = Fx.klass.newInstance('UploadFileToK3C') as UploadFileToK3C
                // 登录云星空
                uploadFileToK3C.login()
                // 遍历附件列表,循环上传附件到云星空
                fileList.each{ item ->
                  Map fileMap = item as Map
                  String npath = fileMap['path'] // npath
                  String fileName = fileMap['filename'] // 文件名
                  uploadFileToK3C.singleUpload(fileName, npath, destObjectApiName, true, destDataId, destDataName)
                }
              } else {
                log.info('同步目标数据结果失败, 失败接口返回的错误信息为:' + msg)
              }
            }
            def resultRet = new IntegrationStreamSync.AfterSyncResult()
            resultRet.setCode("0") // 返回结果状态 0为正常,其他都是失败
            resultRet.setMessage("同步后函数执行成功") // 返回的错误信息
            return resultRet
        }