本文最后更新于 2025年4月28日, 如有失效请评论区留言。
背景
最近公司业务准备将签订协议这块, 接入电子合同平台, 于是研究了下法大大. 需求比较简单, 公司为甲方, 发起签署后, 由用户(乙方)填写姓名, 手机号, 手写签名, 提交后甲方自动落章, 完成签署.
准备
印章:
印章详情免验签添加集成应用的免签场景码
集成应用:
添加免签场景, 再把场景码添加到印章详情中
流程
代码流程
- 导入sdk包
- 初始化客户端
- 获取token
- 文件处理
- 获取文件上传地址 有效期30分钟
- 上传本地文件到上传地址
- 处理文件得到fileId
- 查询文档关键字坐标
- 查询印章
- 签署处理
- 创建签署任务
- 获取参与方专属链接
type SvipSign interface {
Download(ctx context.Context, signFlowId string) (string, error)
GetVipSignUrl(ctx context.Context, phone, fileName, filePath, companyName, ucid string) (string, error)
}
// 法大大
type fddSign struct {
}
func NewFddSign() SvipSign {
return &fddSign{}
}
func (v *fddSign) GetVipSignUrl(ctx context.Context, phone, fileName, filePath, companyName, ucid string) (string, error) {
var businessId string // 免验证签场景码
// 1.初始化
cli := client.NewClient(
app.Config.FddAppid,
app.Config.FddAppSecret,
app.Config.FddHost,
)
// 2.获取token 有效期2小时
accessTokenRes := cli.GetAuthToken()
if accessTokenRes.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("获取token失败: %s", accessTokenRes.Msg))
}
token := accessTokenRes.Data.AccessToken
// 3.获取文件上传地址 有效期30分钟
res := cli.GetLocalFileUploadUrl(&docProcessRequestModel.GetLocalUploadFileUrlReq{
FileType: "doc",
}, token)
if res.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("获取获取文件上传地址失败: %s", res.Msg))
}
// 4.上传本地文件
err := cli.PutFileUpload(filePath, res.Data.UploadUrl)
if err != nil {
return "", errors.GenericError(fmt.Sprintf("上传本地文件失败: %s", err))
}
// 5.文件处理-获取文件fileId
fileProcessRes := cli.FileProcess(&docProcessRequestModel.FileProcessReq{
FddFileUrlList: []docProcessRequestModel.FddUploadFile{
{
FileType: "doc",
FddFileUrl: res.Data.FddFileUrl, // 存储空间中的源文件地址
FileName: fileName,
},
},
}, token)
if fileProcessRes.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("文件处理失败: %s", fileProcessRes.Msg))
}
// 6.查询文档关键字坐标-用作签字
getKeywordPositionRes := cli.GetKeywordPositions(&docProcessRequestModel.GetKeywordPositionReq{
FileId: fileProcessRes.Data.FileIdList[0].FileId,
Keywords: []string{KEYWORD_JIA, KEYWORD_YI, KEYWORD_PHONE, KEYWORD_WEICHAT, KEYWORD_SIGN, KEYWORD_DATE},
}, token)
if getKeywordPositionRes.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("查询文档关键字坐标失败: %s", getKeywordPositionRes.Msg))
}
keywordsFields := keywordsConversion(getKeywordPositionRes.Data)
// 7.查询印章列表 - 获取免签的印章, 需反复验证获取
getSealListRes := cli.GetSealList(&sealRequestModel.GetSealListReq{
OpenCorpId: app.Config.FddOpenCorpid,
ListFilter: &sealRequestModel.SealListFilter{
CategoryType: []string{"official_seal", "contract_seal"}, // 印章类型: official_seal公章 contract_seal合同印章
},
}, token)
if getSealListRes.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("查询印章列表失败: %s", getSealListRes.Msg))
}
printInfo("查询印章列表", getSealListRes.Data)
// 循环查询印章详情 直到获得到 businessId
for _, seal := range getSealListRes.Data.SealInfos {
// 过滤掉非报考公司的印章
if seal.SealName != "公司名称" || seal.SealStatus != "enable" {
continue
}
sealID, _ := strconv.ParseInt(seal.SealId, 10, 64)
// 查询印章详情 - 获取 BusinessId
getSealDetailRes := cli.GetSealDetail(&sealRequestModel.GetSealDetailReq{
OpenCorpId: app.Config.FddOpenCorpid,
SealId: sealID,
}, token)
if getSealDetailRes.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("查询印章详情失败: %s", getSealDetailRes.Msg))
}
printInfo("查询印章详情", getSealDetailRes.Data)
if getSealDetailRes.Data.SealInfo.FreeSignInfos != nil {
businessId = getSealDetailRes.Data.SealInfo.FreeSignInfos[0].BusinessId
fmt.Println("----------businessId: ", businessId)
break
}
}
if businessId == "" {
return "", errors.GenericError("未获取到免验签的印章")
}
// 8. CreateSignTask 创建签署任务
createSignTaskRes := cli.CreateSignTask(&signtaskRequestModel.CreateSignTaskReq{
Initiator: &commonModel.OpenId{
OpenId: app.Config.FddOpenCorpid,
IdType: "corp",
},
SignTaskSubject: "签署标题",
AutoStart: true, // 是否自动提交签署任务
AutoFillFinalize: true, // 自动定稿
AutoFinish: true,
BusinessId: businessId, // 免验证签场景码 配合 RequestVerifyFree
SignInOrder: true, // 签署顺序 配合OrderNo
Actors: []commonModel.SignTaskActor{ // 参与方列表
{
Actor: &commonModel.Actor{
ActorType: "corp",
ActorFDDId: app.Config.FddActorid, // 企业法大大号 - 免验签所需
ActorId: "甲方",
ActorName: "公司名称",
Permissions: []string{"sign"},
},
SignFields: []commonModel.SignField{
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_JIA + "1", // 控件编码
},
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_DATE + "0", // 控件编码
},
},
SignConfigInfo: &commonModel.SignConfigInfo{
OrderNo: 2, // 签署顺序
RequestVerifyFree: true, // 是否请求免验证签 配合BusinessId
JoinByLink: true, // 允许企业参与方任意成员通过链接打开签署任务
},
},
{
Actor: &commonModel.Actor{
ActorType: "person",
ActorId: PERSON_ACTOR_ID,
ActorName: "签署人姓名",
NotifyAddress: "签署人手机号", // 签署人手机号
ActorOpenId: "0a105cb00a2041b9964ba332d76e9d5b", // 个人用户OpenId 获取下载链接时需要
Permissions: []string{"sign", "fill"},
SendNotification: true,
NotifyType: []string{"start", "finish"},
},
FillFields: []commonModel.FillField{ // 填写控件
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_YI + "0", // 控件编码
},
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_PHONE + "0", // 控件编码
},
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_WEICHAT + "0", // 控件编码
},
},
SignFields: []commonModel.SignField{ // 签章控件
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_SIGN + "0", // 控件编码
},
{
FieldDocId: fileProcessRes.Data.FileIdList[0].FileId, // 控件所在的文档标识
FieldId: KEYWORD_DATE + "1", // 控件编码
},
},
SignConfigInfo: &commonModel.SignConfigInfo{
OrderNo: 1,
JoinByLink: true,
},
},
},
Docs: []commonModel.Doc{
{
DocId: fileProcessRes.Data.FileIdList[0].FileId,
DocName: fileProcessRes.Data.FileIdList[0].FileName,
DocFileId: fileProcessRes.Data.FileIdList[0].FileId,
DocFields: keywordsFields, // 文档中添加控件
},
},
}, token)
if createSignTaskRes.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("创建签署任务失败: %s", createSignTaskRes.Msg))
}
printInfo("创建签署任务", createSignTaskRes.Data)
// 9.获取参与方专属链接
getSignTaskActorUrl := cli.GetSignTaskActorUrl(&signtaskRequestModel.SignTaskActorGetUrlReq{
SignTaskId: createSignTaskRes.Data.SignTaskId,
ActorId: PERSON_ACTOR_ID,
}, token)
if getSignTaskActorUrl.Code != CODE_SUCCESS {
return "", errors.GenericError(fmt.Sprintf("获取参与方专属链接失败: %s", getSignTaskActorUrl.Msg))
}
printInfo("获取参与方专属链接", getSignTaskActorUrl.Data)
return getSignTaskActorUrl.Data.ActorSignTaskUrl, nil
}
// keywordsConversion 将关键字转换为发起创建所需的定位控件信息 keywords-关键字定位信息
func keywordsConversion(keywords []docProcessResponseModel.GetKeywordPositionData) []commonModel.Field {
if len(keywords) == 0 {
return nil
}
var fields []commonModel.Field
for _, v := range keywords {
fieldType := "text_single_line" // 单行文本
// 最后一个甲方关键字
if v.Keyword == KEYWORD_JIA {
fieldType = "corp_seal" // 企业印章
}
if v.Keyword == KEYWORD_SIGN {
fieldType = "person_sign" // 个人签名
}
if v.Keyword == KEYWORD_DATE {
fieldType = "date_sign" // 日期戳
}
for i, position := range v.Positions {
if v.Keyword == KEYWORD_JIA && i != len(v.Positions)-1 {
continue // 只需要最后一个甲方关键字
}
// if (v.Keyword == KEYWORD_DATE && i != 0) || v.Keyword == KEYWORD_SIGN {
// continue
// }
floatNum, _ := strconv.ParseFloat(position.Coordinate.PositionX, 64)
fields = append(fields, commonModel.Field{
FieldId: v.Keyword + strconv.Itoa(i),
FieldName: v.Keyword,
FieldType: fieldType,
Position: &commonModel.FieldPosition{
PositionMode: "pixel", // 定位模式 pixel: 像素值
PositionPageNo: position.PositionPageNo,
PositionX: strconv.FormatFloat(floatNum+float64(130), 'f', -1, 64),
PositionY: position.Coordinate.PositionY,
},
FieldTextSingleLine: &commonModel.FieldTextSingleLine{
Required: true,
},
FieldCorpSeal: &commonModel.FieldCorpSeal{
CategoryType: "official_seal",
},
})
}
}
jsonData, _ := json.MarshalIndent(fields, "", " ")
fmt.Println("----------文档中添加控件坐标:\n", string(jsonData))
return fields
}
func printInfo(msg string, v any) {
jsonData, _ := json.MarshalIndent(v, "", " ")
fmt.Printf("----------%s:%s\n", msg, string(jsonData))
}