电话 400-101-6950

# 基于PDF文档进行腾讯电子签署

# 使用场景说明

1.直接在系统上上传合同文件和签署方等必要信息

image.png

2、基于已经上传的pdf文件生成进行签署

image.png

使用说明:
1、发起方需要通过生成经办人的方式,先生成经办人(相当于合同发起人)后才可以发起合同签署
2、若签署方是企业,则签署方需提供【签署人姓名、手机号、签署单位名称】
3、若签署方的企业没有在腾讯电子签中认证过,那么收到合同后,会进入企业认证流程,第一个进行企业认证的人会成为企业腾讯电子签平台的超管

# 一、前置配置

1、需要在道一平台下单购买腾讯电子签份额
2、需要使用腾讯电子签连接器,配置控制台地址

用于生成用户登录进入签署文件后台连接入口
第一次需要注册验证子客企业/身份认证

image.png

3、需要使用腾讯电子签连接器,配置经办人(企业方的发起签署、签署文件均需要配置经办人/签署人)

image.png

# 二、完成前置配置后,可使用脚本实现PDF文件签署

1、脚本核心逻辑概览:
根据场景使用脚本构造PDF签署流程对象https://qian.tencent.com/developers/partnerApis/startFlows/ChannelCreateFlowByFiles/ (opens new window)

2、整体流程如下:

  1. 获取当前表单文档
  2. 上传合同 PDF 文件,获取腾讯电子签 fileId
  3. 构建创建签署流程的 DTO
  4. 读取签署方数量,动态生成签署人数组
  5. 为每一方签署人配置:
  • 基础信息(姓名 / 手机号 / 通讯录)
  • 签署方类型(个人 / 企业 / 代签)
  • 认证方式
  • 通知方式
  • 签署控件 / 填写控件
  1. 调用腾讯电子签接口发起签署流程
  2. 返回签署结果

# 三、关键方法说明

  1. 上传签署文件 uploadSignFile
  • 从表单中读取 合同文件
  • 仅支持 PDF 格式
  • 若存在多个文件,默认取 第一个文件
  • 返回腾讯电子签侧的 fileId
  1. 填充签署人基础信息 fillSignerBaseInfo
  • 支持手工填写签署人姓名、手机号
  • 若选择了通讯录用户,则:
    • 自动覆盖姓名、手机号
    • 通讯录信息优先生效
  1. 设置签署方类型 fillApproverType

支持的签署方类型映射关系如下:

表单值 签署方类型
1 PERSON (个人)
2 PERSON_AUTO_SIGN(个人自动签)
3 ORGANIZATION(企业)
4 ENTERPRISESERVER (企业代签)

当签署方为 企业 / 企业代签 时,需额外配置:

  • 企业 openId
  • 企业组织 ID(可选)
  • 企业名称(可选)
  1. 设置认证方式 fillApproverSignTypes
  • 支持配置多种认证方式
  • 表单中以数组形式维护
  • 自动转换

# 四、使用PDF发起签署-demo

操作步骤: 1、把应用包导入到客户环境 2、导入基础组件定义数据包 3、配置合同,即可发起合同签署

demo应用包: https://xinyuninfo.feishu.cn/wiki/CErUwBPXoiJAdMkGIcEcHHaHnNd?fromScene=spaceOverview#share-BX06dr2wOokm6cxDgUQcFbBWnOg (opens new window)

基础组件定义数据包:组件定义数据.xlsx (opens new window)

image.png

demo-脚本

image.png

(function () {

    /** 当前表单文档 */
    var currentDoc = $.context.getCurrentDocument();

    /** 上传签署文件,返回腾讯云 fileId */
    var signFileId = uploadSignFile(currentDoc);

    /** 当前应用 ID */
    var applicationId = $.context.getCurrentApplicationId();

    /** 构建创建签署流程 DTO */
    var createFlowDTO = new Packages.cn.com.do1.do1cloud.signature.model.dto.ChannelCreateFlowByFileDTO();

    // 合同发起人(经办人 openId)
    createFlowDTO.setOpenId(currentDoc.getElementByName("经办人").getValue());

    // 合同名称
    createFlowDTO.setFlowName(currentDoc.getElementByName("合同名称").getValue());

    // **0**:在合同流程发起时,由发起人指定签署方的签署控件的位置和数量。
    // **1**:签署方在签署时自行添加签署控件,可以拖动位置和控制数量。
    // 注**: `发起后添加控件功能不支持添加签批控件`
    var signBeanTag = currentDoc.getElementByName("是否支持拖拽签署区域").getValue()
    if (signBeanTag == "2") {
        createFlowDTO.setSignBeanTag(1)
    }

    // 腾讯电子签:目前仅支持单文件签署
    var fileIds = java.lang.reflect.Array.newInstance(java.lang.String, 1);
    fileIds[0] = signFileId;
    createFlowDTO.setFileIds(fileIds);

    // 是否乱序签署
    var unordered = currentDoc.getElementByName("是否乱序签署").getValue()
    if (unordered == "2") {
        createFlowDTO.setUnordered(true);
    }

    /**
     * 合同发起方填写信息
     */
    var contractInitiatorSignerDocList = currentDoc.getElementByName("合同发起方填写组件配置").getSubDocuments();
    /** 合同发起方填写(非必填) */
    var contractInitiatorSignComponents = generateSignComponent(contractInitiatorSignerDocList);
    if (contractInitiatorSignComponents != null) {
        createFlowDTO.setComponents(contractInitiatorSignComponents);
    }

    /**
     * 签署方
     * */
    var signerDocList = currentDoc.getElementByName("签署方").getSubDocuments();
    if (signerDocList == null || signerDocList.size() < 1) {
        $.log.warn("合同签署方为空,无需发起签署");
        return;
    }

    var signerCount = signerDocList.size();
    var signerDocMap = new Packages.java.util.HashMap();
    for (var sin = 1; sin <= signerCount; sin++) {
        var signer = signerDocList.get(sin - 1);
        var sign = signer.getElementByName("签署方").getValue();
        $.log.info("sign:{}", sign)
        $.log.info("sign::{}", signer)
        signerDocMap.put(sign + "", signer);
    }

    /** 构建签署方数组 */
    var approverInfos = java.lang.reflect.Array.newInstance(
        com.tencentcloudapi.essbasic.v20210526.models.FlowApproverInfo,
        signerCount);

    var signerPrefixMap = new Packages.java.util.HashMap();
    signerPrefixMap.put("1", "甲");
    signerPrefixMap.put("2", "乙");
    signerPrefixMap.put("3", "丙");

    for (var i = 1; i <= signerCount; i++) {

        var approverInfo = new Packages.com.tencentcloudapi.essbasic.v20210526.models.FlowApproverInfo();
        var signerDoc = signerDocMap.get(i + "");

        /** 处理签署人基础信息(支持通讯录覆盖) */
        fillSignerBaseInfo(signerDoc, approverInfo);

        /** 设置签署类型 */
        fillApproverType(signerDoc, approverInfo);

        /** 设置签署标识*/
        fillSignatureMark(signerDoc, createFlowDTO);

        /** 设置通知 */
        fillNotifyType(signerDoc, approverInfo);
        /** 认证方式 */
        fillApproverSignTypes(signerDoc, approverInfo);

        /** 设置签署控件 */
        var signComponents = loadComponents(applicationId, "签署人签署组件", currentDoc.getId(), i);
        if (signComponents == null) {
            if (signBeanTag != "2") {
                $.log.warn(signerPrefixMap.get(i + "") + "方签署组件为空,发起签署失败");
                return
            }
        } else {
            approverInfo.setSignComponents(signComponents);
        }

        /** 设置填写控件(非必填) */
        var fillComponents = loadComponents(applicationId, "签署人填写组件", currentDoc.getId(), i);
        if (fillComponents != null) {
            approverInfo.setComponents(fillComponents);
        }
        /** 签署方强制阅读时长 */
        var preReadTime = signerDoc.getElementByName("签署方强制阅读时长").getValue();
        if (preReadTime != null && preReadTime != "") {
            approverInfo.setPreReadTime(new java.lang.Long(preReadTime));
        }

        /** 签署方强制阅读时长 */
        var preReadTime = signerDoc.getElementByName("签署方强制阅读时长").getValue();
        if (preReadTime != null && preReadTime != "") {
            approverInfo.setPreReadTime(new java.lang.Long(preReadTime));
        }
        /** 控制签署方在签署合同时能否进行某些操作 */
        initApproverOption(signerDoc, approverInfo)

        approverInfos[i - 1] = approverInfo;
    }

    createFlowDTO.setFlowApproverInfos(approverInfos);

    /** 调用腾讯电子签接口 */
    var result = $.electronicsignature.channelCreateFlowByFiles(createFlowDTO);
    $.log.warn("发起签署结果: {}", $.json.objectToJsonString(result));

    return $.json.objectToJsonString(result);

})();

/**
 * 个人自动签署,设置自动签署标识
 */
function fillSignatureMark(doc, createFlowDTO) {
    var type = doc.getElementByName("签署方类型").getValue()
    var autoSignScene = doc.getElementByName("个人自动签署标识").getValue();
    var autoSignSceneMap = {
        1: "E_PRESCRIPTION_AUTO_SIGN",
        2: "OTHER"
    };
    if (type == 2) {
        if (autoSignScene == null || autoSignScene == "") {
            $.log.warn("个人自动签署标识空");
            return;
        }
        createFlowDTO.setAutoSignScene(autoSignSceneMap[autoSignScene])
    }
}

/**
 * 控制签署方在签署合同时能否进行某些操作
 * @param currentDoc
 * @param approverInfo
 */
function initApproverOption(doc, approverInfo) {
    var approverOption = new Packages.com.tencentcloudapi.essbasic.v20210526.models.ApproverOption();
    var needSetApproverOption = false;
    var noRefuse = doc.getElementByName("是否可以拒签").getValue();
    if (noRefuse == 2) {
        approverOption.setNoRefuse(true);
        needSetApproverOption = true;
    } else {
        approverOption.setNoRefuse(false);
    }
    var noTransfer = doc.getElementByName("是否可以转发").getValue();
    if (noTransfer == 2) {
        needSetApproverOption = true;
        approverOption.setNoTransfer(true);
    } else {
        approverOption.setNoTransfer(false);
    }
    var hideOneKeySign = doc.getElementByName("是否隐藏一键所有的签署区").getValue();
    if (hideOneKeySign == 1) {
        needSetApproverOption = true;
        approverOption.setHideOneKeySign(true);
    } else {
        approverOption.setHideOneKeySign(false);
    }
    var fillType = doc.getElementByName("签署人信息补充类型").getValue();
    if (fillType == 1) {
        needSetApproverOption = true;
        approverOption.setFillType(1);
    }

    var flowReadLimit = doc.getElementByName("签署人阅读合同限制").getValue();

    var flowReadLimitMap = {
        1: "LimitReadTimeAndBottom",
        2: "LimitReadTime",
        3: "LimitBottom",
        4: "NoReadTimeAndBottom"
    };
    if (flowReadLimit != null && flowReadLimit != "" && flowReadLimitMap.get(flowReadLimit) != null) {
        needSetApproverOption = true;
        approverOption.setFlowReadLimit(flowReadLimitMap[flowReadLimit]);
    }
    if (needSetApproverOption) {
        approverInfo.setApproverOption(approverOption);
    }
}

/**
 * 填充签署人姓名、手机号
 * 如果填写了通讯录用户,则优先使用通讯录信息
 */
function fillSignerBaseInfo(doc, approverInfo) {
    $.log.info("dd:{}", doc)
    var userId = doc.getElementByName("签署人").getValue();
    var userName = doc.getElementByName("签署人姓名").getValue();
    var phone = doc.getElementByName("签署人手机号").getValue();

    if (userId != null && userId != "") {
        var user = $.contact.getUserById(userId);
        userName = user.getName();
        phone = user.getTelephone();
    }

    approverInfo.setName(userName);
    approverInfo.setMobile(phone);
}

/**
 * 设置签署方类型及企业信息
 */
function fillApproverType(doc, approverInfo) {

    var type = doc.getElementByName("签署方类型").getValue();

    var typeMap = {
        1: "PERSON",
        2: "PERSON_AUTO_SIGN",
        3: "ORGANIZATION",
        4: "ENTERPRISESERVER"
    };

    approverInfo.setApproverType(typeMap[type]);

    // 企业 / 企业代签
    if (type == 3 || type == 4) {
        $.log.warn("签署方式企业: {}", type);
        $.log.warn("签署方openId: {}", doc.getElementByName("签署人openid").getValue());
        approverInfo.setOpenId(doc.getElementByName("签署人openid").getValue());

        var orgId = doc.getElementByName("签署人机构id").getValue();
        if (orgId) {
            approverInfo.setOrganizationOpenId(orgId);
        }

        var orgName = doc.getElementByName("签署人机构名称").getValue();
        if (orgName) {
            approverInfo.setOrganizationName(orgName);
        }
    }
}

/**
 * 设置签署人签署合同时的认证方式
 */
function fillApproverSignTypes(doc, approverInfo) {

    var approverSignTypes = doc.getElementByName("签署人签署合同时的认证方式").getValue();
    if (approverSignTypes != null && approverSignTypes.size() > 0) {
        var size = approverSignTypes.size()
        var arr = java.lang.reflect.Array.newInstance(java.lang.Long, size);
        for (var ii = 0; ii < size; ii++) {
            $.log.info("approverSignTypes: {}",approverSignTypes.get(ii))
            
            arr[ii] = new Packages.java.lang.Long(approverSignTypes.get(ii));
        }
        approverInfo.setApproverSignTypes(arr);
    }
}

/**
 * 设置签署通知方式
 */
function fillNotifyType(doc, approverInfo) {

    var notifyType = doc.getElementByName("通知签署方经办人的方式").getValue();

    if (notifyType == "1") {
        approverInfo.setNotifyType("SMS");
    } else if (notifyType == "2") {
        approverInfo.setNotifyType("NONE");
    }
}

/**
 * 根据表单配置生成签署/填写组件
 */
function loadComponents(appId, formName, contractId, signerIndex) {

    var queryMap = new Packages.java.util.HashMap();
    queryMap.put("关联签署合同", contractId);
    queryMap.put("签署方", signerIndex + "");

    var documents = $.form.getFormDocumentsByFieldNameAndValue(appId, formName, queryMap);
    if (documents == null || documents.size() < 1) {
        return null;
    }

    return generateSignComponent(documents);
}

/**
 * 将“组件配置表单”转换为腾讯电子签 Component 数组
 */
function generateSignComponent(documentList) {

    var size = documentList.size();
    var components = java.lang.reflect.Array.newInstance(com.tencentcloudapi.essbasic.v20210526.models.Component, size);

    var componentTypeMap = {
        "1": "TEXT",
        "2": "MULTI_LINE_TEXT",
        "3": "SIGN_SEAL",
        "4": "SIGN_DATE",
        "5": "SIGN_PAGING_SEAL",
        "6": "SIGN_OPINION",
        "7": "SIGN_VIRTUAL_COMBINATION",
        "8": "SIGN_MULTI_LINE_TEXT",
        "9": "SIGN_SELECTOR",
        "10": "SIGN_LEGAL_PERSON_SEAL",
        "11": "DATE",
        "12": "DISTRICT",
        "13": "SIGN_SIGNATURE",
        "14": "CHECK_BOX",
        "15": "FILL_IMAGE",
        "16": "ATTACHMENT",
        "17": "SELECTOR",
        "18": "VIRTUAL_COMBINATION",
        "19": "WATERMARK",
    };

    for (var i = 0; i < size; i++) {

        var doc = documentList.get(i);
        var component = new Packages.com.tencentcloudapi.essbasic.v20210526.models.Component();

        component.setComponentType(componentTypeMap[doc.getElementByName("组件类型").getValue()]);
        component.setFileIndex(0);

        var componentId = doc.getElementByName("组件id").getValue();
        if (componentId) {
            component.setComponentId(componentId);
        }

        var componentName = doc.getElementByName("组件标题").getValue();
        if (componentName) {
            component.setComponentName(componentName);
        }

        fillComponentPosition(component, doc);
        fillComponentKeyword(component, doc);
        fillComponentCommon(component, doc);

        components[i] = component;
    }

    return components;
}

/**
 * 填充组件的定位信息(非关键字生成方式)
 *
 * 适用场景:
 *  - NORMAL:普通坐标定位
 *  - FIELD:表单字段定位
 *
 * 包含内容:
 *  - 组件生成方式
 *  - X / Y 坐标
 *  - 页码
 *
 * 注意:
 *  - 当生成方式为 KEYWORD(关键字)时,本方法只负责设置 GenerateMode
 *  - 具体关键字相关逻辑由 fillComponentKeyword 处理
 *
 */
function fillComponentPosition(component, doc) {

    var generateMode = doc.getElementByName("组件生成方式").getValue();

    if (generateMode == 1) {
        component.setGenerateMode("NORMAL");
    } else if (generateMode == 2) {
        component.setGenerateMode("FIELD");
    } else if (generateMode == 3) {
        component.setGenerateMode("KEYWORD");
        return;
    }

    // 横坐标
    var x = doc.getElementByName("组件横坐标").getValue();
    if (x != null && x != "") {
        component.setComponentPosX(new java.lang.Float(x));
    }

    // 纵坐标
    var y = doc.getElementByName("组件纵坐标").getValue();
    if (y != null && y != "") {
        component.setComponentPosY(new java.lang.Float(y));
    }

    // 页码
    var page = doc.getElementByName("组件在第几页").getValue();
    if (page != null && page != "") {
        component.setComponentPage(new java.lang.Long(page));
    }
}

/**
 * 填充组件的关键字定位相关属性
 *
 * 仅在生成方式为 KEYWORD 时生效:
 *  - 横纵偏移量
 *  - 关键字排序规则
 *  - 关键字索引
 *  - 相对位置
 *
 * 设计原则:
 *  - 若生成方式不是 KEYWORD,直接 return
 *  - 所有字段均为“可选配置”,避免空值导致异常
 */
function fillComponentKeyword(component, doc) {

    var generateMode = doc.getElementByName("组件生成方式").getValue();
    if (generateMode != 3) {
        return;
    }

    // 关键字横向偏移
    var offsetX = doc.getElementByName("关键字横坐标偏移").getValue();
    if (offsetX != null && offsetX != "") {
        component.setOffsetX(new java.lang.Float(offsetX));
    }

    // 关键字纵向偏移
    var offsetY = doc.getElementByName("关键字纵坐标偏移").getValue();
    if (offsetY != null && offsetY != "") {
        component.setOffsetY(new java.lang.Float(offsetY));
    }

    // 关键字排序规则
    var keywordOrder = doc.getElementByName("关键字排序规则").getValue();
    if (keywordOrder == "1") {
        component.setKeywordOrder("Positive");
    } else if (keywordOrder == "2") {
        component.setKeywordOrder("Reverse");
    }

    // 关键字索引(如:[1,2,3])
    var keywordIndexesStr = doc.getElementByName("关键字索引").getValue();
    var keywordIndexes = parseLongArray(keywordIndexesStr);
    $.log.info("keywordIndexes:  {} ", $.json.objectToJsonString(keywordIndexes))
    if (keywordIndexes != null && keywordIndexes.length > 0) {
        component.setKeywordIndexes(keywordIndexes);
    }

    // 关键字生成区域的相对位置
    var relativeLocation = doc.getElementByName("关键字生成的区域的对齐方式").getValue();
    if (relativeLocation == 1) {
        component.setRelativeLocation("Middle");
    } else if (relativeLocation == 2) {
        component.setRelativeLocation("Below");
    } else if (relativeLocation == 3) {
        component.setRelativeLocation("Right");
    } else if (relativeLocation == 4) {
        component.setRelativeLocation("LowerRight");
    } else if (relativeLocation == 5) {
        component.setRelativeLocation("UpperRight");
    }
}

function parseLongArray(value) {
    if (value == null || value == "") {
        return null;
    }

    $.log.info("value:  {} ", value)
    var str = new java.lang.String(value).trim();
    $.log.info("str.length:  {} ", str.length())
    var content = str.substring(1, str.length() - 1);
    if (content == "") {
        return java.lang.reflect.Array.newInstance(java.lang.Long, 0);
    }

    var parts = content.split(",");
    var arr = java.lang.reflect.Array.newInstance(java.lang.Long, parts.length);

    for (var i = 0; i < parts.length; i++) {
        arr[i] = java.lang.Long.valueOf(parts[i].trim());
    }
    return arr;
}

/**
 * 填充电子签组件的公共属性
 */
function fillComponentCommon(component, doc) {

    component.setComponentWidth(new java.lang.Float(doc.getElementByName("组件宽度").getValue()));
    component.setComponentHeight(new java.lang.Float(doc.getElementByName("组件高度").getValue()));

    var required = doc.getElementByName("组件是否必填").getValue();
    component.setComponentRequired(required == "1");

    var defaultValue = doc.getElementByName("组件默认值").getValue();
    if (defaultValue) {
        component.setComponentValue(defaultValue);
    }

    var ext = doc.getElementByName("组件的扩展参数").getValue();
    if (ext) {
        component.setComponentExtra(ext);
    }
}

/**
 * 上传签署文件到腾讯云,如果七巧的文件存在多个,也只会默认拿第一个文件进行签署
 * 文件格式仅支持PDF
 * @param doc
 * @returns {*}
 */
function uploadSignFile(doc) {
    // 这里默认认为是必须要有签署文件的
    var jsonArray = doc.getElementByName("合同文件").getValue();
    var jsonObject = jsonArray.getJSONObject(0);
    //构建文件对象
    var uploadFileDTO = new Packages.cn.com.do1.do1cloud.signature.model.dto.UploadFileDTO();
    // 这里默认认为是必须要有经办人的,对应Plus的人员id,可以传入人员选的值
    var openId = doc.getElementByName("经办人").getValue();
    uploadFileDTO.setOpenId(openId);
    // 获取文件上传的fileId
    var fileId = jsonObject.get("fileId")
    uploadFileDTO.setFileId(fileId);
    var uploadFilesResponse = $.electronicsignature.uploadFiles(uploadFileDTO);

    $.log.info("上传腾讯签署文件返回结果:{}", $.json.objectToJsonString(uploadFilesResponse))

    // 获取腾讯云上传后得到要签署的文件id
    return uploadFilesResponse.getFileIds()[0];
}


# 五、附录

# 问题1:签署方配置问题

1、单方签署时,只允许甲方作为签署人
2、双方签署时:只允许选 甲、乙 双方
3、三方签署时:只允许选 甲、乙、丙 三方
4、每一方签署有且只有一人,不能同时存在多个甲方、多个乙方、多个丙方

# 问题2:签署短信通知问题

签署人配置为:企业方时,不会触发短信通知

# 问题3:签批控件要求

签批控件的子控件为签名控件,子控件的横坐标要小于父控件的横坐标加宽度,纵坐标要小于父控件的纵坐标加高度
父控件的扩展参数:{"Children":["ces_1"]} 备注:ces_1为子控件的组件id

# 问题4:虚拟控件要求

虚拟控件,子控件只能用勾选框控件,虚拟控件的扩展参数:
{"SubType":"CHECK_BOX_GROUP","MultiSelect":true,"Children":["ComponentId_11","ComponentId_10"]}
备注:ComponentId_10,ComponentId_11为子控件的组件id

# 问题5:选择控件要求

选项的位置要在选择控件的范围内

自定义上传PDF文件签署:

  • 当签署人是自然人非企业时,才可以发送短信
  • 签署控件支持拖拽的话,则签署方在签署时自行添加签署控件,不需要添加签署控件
  • 签署方填写控件不允许有默认值
  • 填写组件若同名,有可能出现同步修改的问题,建议不要写同名的填写组件
1 / 0