Golang系列 电子合同平台之法大大浅用
本文最后更新于 2025年4月28日, 如有失效请评论区留言。

背景

最近公司业务准备将签订协议这块, 接入电子合同平台, 于是研究了下法大大. 需求比较简单, 公司为甲方, 发起签署后, 由用户(乙方)填写姓名, 手机号, 手写签名, 提交后甲方自动落章, 完成签署.

准备

测试环境接入准备

印章:

image-20250428162220356

印章详情免验签添加集成应用的免签场景码

image-20250428163217218

集成应用:

image-20250428162542213

image-20250428162809582

添加免签场景, 再把场景码添加到印章详情中

image-20250428162938914

流程

参考教培机构管理系统在线签署购课合同(模板发起)

api文档

代码流程

  • 导入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))
}

原创声明
本文由 Twist 于2025年4月28日 发表在 柯基屁屁
如未特殊声明,本站所有文章均为原创;你可以在保留作者及原文地址的情况下转
转载请注明:Golang系列 电子合同平台之法大大浅用 | 柯基屁屁
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇