从零开始构建一个Go语言的SDK框架:实现e签宝合同签署

项目概述

在企业级应用开发中,电子签名服务已经成为不可或缺的一部分,e签宝作为国内领先的电子签名服务提供商,为开发者提供了丰富的API接口。由于工作项目中需要使用go语言对接e签宝合同业务,目前也没找到第三方好用的e签宝go语言SDK,所以,干脆自己从零手写一个SDK,把项目中踩过的坑也汇总整理一下,方便后来人参考使用。同时,本项目也可以作为其它SDK的框架模板,稍作改动就可以用于其它业务。

本文将以这个 e签宝Go SDK为例,展示如何从零开始构建一个完整的Go语言SDK框架。本项目提供了完整的API封装,包括认证管理、模板处理、账户管理、签署流程等功能。完整代码在文末。

1. 整体目录结构

esign-go-sdk/
├── api/               # API服务实现
│   ├── account_api/   # 账户相关API
│   ├── auth_api/      # 认证相关API
│   ├── sign_api/      # 签署流程API
│   ├── template_api/  # 模板相关API
│   └── api_base.go    # API基础常量和工具
├── config/            # 配置相关代码
│   ├── esign_config.go  # e签宝配置
│   └── redis_config.go  # Redis缓存配置
├── initialize/        # 初始化工具
│   └── tests_init.go  # 测试环境初始化
├── types/             # 数据结构定义
│   └── types.go       # 所有API请求/响应结构体
├── utils/             # 通用工具函数
│   └── utils.go       # JSON处理、日志等工具函数
├── tests/             # 测试用例
├── .env               # 环境变量配置文件
├── .env.demo          # 环境变量配置示例
├── client.go          # SDK主入口
├── go.mod             # Go模块依赖
└── go.sum             # 依赖版本锁定

2. 核心设计理念

SDK采用模块化设计,分层架构,每个模块职责单一。部分设计思想借鉴了go-zero框架。

  • 配置层:统一管理应用配置和Redis连接,以及后续更多组件的配置。
  • 认证层:负责Token获取、缓存和刷新。一般的调用三方服务都会用到鉴权,就统一放到这个认证层。
  • 业务层:提供具体的API服务接口,实现具体的业务逻辑。
  • 工具层:封装通用的HTTP请求和数据处理逻辑,以及常用的工具函数。

核心模块详解

1. 主入口设计

本项目的主入口采用了简洁明了的设计,通过单一的Client结构体聚合所有服务,使用户能够通过一个客户端实例访问所有功能。这种设计风格符合Go语言的组合优于继承的思想。代码:client.go

// e签宝 SDK 的主入口点
type Client struct {
    Auth     *auth_api.AuthService
    Template *template_api.TemplateService
    Account  *account_api.AccountService
    Sign     *sign_api.SignService
}

// NewClient 创建一个新的 e签宝 客户端
func NewClient(cfg *config.Config) *Client {
    // 初始化认证服务
    authService := auth_api.NewAuthService(cfg)
    // 构建并返回主客户端
    return &Client{
        Auth:     authService,
        Template: template_api.NewTemplateService(cfg, authService),
        Account:  account_api.NewAccountService(cfg, authService),
        Sign:     sign_api.NewSignService(cfg, authService),
    }
}

2. 配置管理模块

配置模块采用选项模式(Option Pattern),使配置更加灵活,便于未来扩展。同时集成了环境变量加载功能,支持从.env文件读取配置,提高了配置的安全性和可维护性。代码:config/esign_config.go

// 配置结构体定义
type Config struct {
    AppID       string
    AppSecret   string
    BaseURL     string
    OrgId       string
    GrantType   string
    IsWriteLog  bool
    RedisClient *RedisClient
}

// 选项模式函数类型
type Option func(*Config)

// 配置创建函数
func NewConfig(appID, appSecret, baseURL, orgID, grantType, isWriteLog string, opts ...Option) (*Config, error) {
    // 参数验证和类型转换
    isWriteLogBool, err := strconv.ParseBool(isWriteLog)
    if err != nil {
        return nil, fmt.Errorf("isWriteLog配置异常:%w", err)
    }

    // Redis连接初始化
    redisClient := NewRedisClient("127.0.0.1:6379", "", 0)
    if err = redisClient.Ping(); err != nil {
        defer redisClient.Close()
        return nil, fmt.Errorf("Redis连接失败:%w", err)
    }

    // 构建配置对象
    conf := &Config{
        AppID:       appID,
        AppSecret:   appSecret,
        BaseURL:     baseURL,
        OrgId:       orgID,
        GrantType:   grantType,
        IsWriteLog:  isWriteLogBool,
        RedisClient: redisClient,
    }
    
    // 应用选项
    for _, opt := range opts {
        opt(conf)
    }
    return conf, nil
}

3. 服务分层设计

本项目采用了清晰的服务分层结构,每个API服务都实现了对应的接口,确保了代码的可测试性和可扩展性。各服务之间通过依赖注入的方式进行交互,降低了耦合度。

以账号服务为例:代码:api/account_api/account.go

// AccountServiceInterface 账户服务接口
type AccountServiceInterface interface {
    GetESignPersonsIdentityInfo(psnAccount string, writeLog bool) (eSignPersonsIdentityData *types.PersonsIdentityData, err error)
}

var _ AccountServiceInterface = (*AccountService)(nil)

// AccountService 账户服务
type AccountService struct {
    config      *config.Config
    httpClient  *http.Client
    authService *auth_api.AuthService // 持有认证服务的引用,用于获取 token
}

4. 数据类型定义

所有API请求和响应的数据结构都集中定义在types包中,使代码结构更加清晰。这些结构体严格按照e签宝API文档进行定义,确保了数据序列化和反序列化的准确性。代码:types/types.go

// ESignCommonResponse e签宝通用响应结构体
type ESignCommonResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    any    `json:"data"`
}

// GetESignTokenRequest 获取e签宝token的请求体
type GetESignTokenRequest struct {
    AppId     string `json:"appId"`
    Secret    string `json:"secret"`
    GrantType string `json:"grantType,default:client_credentials"`
}

// ...省略部分代码...

5. utils工具包

工具函数包提供了一系列辅助功能,如JSON序列化和反序列化、日志记录等,这些通用功能的抽离提高了代码的复用性。代码:utils/http.go

// ...省略部分代码...

// SendHttpPostRequest 发送POST请求
func SendHttpPostRequest(requestUrl string, requestBody interface{}, requestHeaders map[string]string, isWriteLog bool) (string, error) {
	// 将请求体转换为 JSON
	bodyBytes, err := json.Marshal(requestBody)
	if err != nil {
		return "", errors.New("请求体转换为JSON失败:" + err.Error())
	}

	// 创建一个新的 POST 请求
	req, err := http.NewRequest("POST", requestUrl, bytes.NewBuffer(bodyBytes))
	if err != nil {
		return "", errors.New("创建POST请求失败:" + err.Error())
	}

	// 设置请求头
	if requestHeaders != nil {
		for key, value := range requestHeaders {
			req.Header.Set(key, value)
		}
	}

	// 使用默认的 HTTP 客户端发送请求
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		logx.Errorf("SendHttpPostRequest发送请求失败: %v", err)
		return "", errors.New("发送POST请求失败:" + err.Error())
	}
	defer resp.Body.Close()

	// 读取响应内容
	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		logx.Errorf("SendHttpPostRequest读取响应内容失败: %v", err)
		return "", errors.New("读取响应内容失败:" + err.Error())
	}
	respBodyStr := string(respBody)

	if isWriteLog { //是否写入日志
		logData := map[string]interface{}{
			"requestUrl":       requestUrl,
			"requestHeaders":   requestHeaders,
			"requestBody":      requestBody,
			"responseHttpCode": resp.StatusCode, //HTTP 状态码
			"responseData":     respBodyStr,
		}
		LogxInfow(logData, "SendHttpPostRequestLog")
	}
	return respBodyStr, nil
}

utils/utils.go 中封装了常用的工具函数,比如 JSON转换和写入日志的方法:

// ...省略部分代码...

// JsonUnmarshalToStruct 将JSON字符串解析为指定的结构体
func JsonUnmarshalToStruct(jsonData any, obj any) error {
	// 先检查jsonData的实际类型
	switch data := jsonData.(type) {
	case string:
		// 如果是字符串,直接 Unmarshal
		if jsonData == "" || jsonData == "[]" || jsonData == "{}" {
			return nil
		}
		err := json.Unmarshal([]byte(data), obj)
		if err != nil {
			return err
		}
	case map[string]any:
		// 如果是map,需要先转换为 JSON 再 Unmarshal
		jsonByte, err := json.Marshal(data)
		if err != nil {
			return err
		}
		err = json.Unmarshal(jsonByte, obj)
	case nil:
		return nil
	default:
		return fmt.Errorf("JsonUnmarshalToStruct: unexpected data type: %T", data)
	}
	return nil
}

// GetCurrentTime 获取当前时间,格式为"年月日时分秒",例如: 20250911112713
func GetCurrentTime() string {
	return time.Now().Format("20060102150405")
}

// 写入日志方法封装
func LogxInfow(logData any, logTitle string) {
	logx.Infow(
		"",                              //JsonMarshalNoEscape(logData),
		logx.Field("contents", logData), //直接存储原始数据而不是 JSON 字符串
		logx.Field("title", logTitle),
		logx.Field("data_type", fmt.Sprintf("%T", logData)),
	)
}

核心功能实现

1. 认证服务模块

认证服务是SDK的基础,负责token的获取、刷新和缓存。该模块实现了认证信息的缓存机制,通过Redis存储token,避免频繁请求e签宝API,提高了SDK的性能。

// 认证服务
type AuthService struct {
    config    *config.Config
    client    *http.Client
    expiresAt time.Time
    token     string
}

// Token获取主逻辑
func (s *AuthService) GetESignToken(useCache bool) (token string, err error) {
    // 优先从缓存获取
    if useCache {
        token, err = s.GetESignTokenFromCacheData()
        if err != nil {
            return "", err
        }
        if token != "" {
            logx.Infof("从缓存获取token成功")
            return token, nil
        }
    }

    // 缓存未命中,从服务器获取
    eSignTokenResp, err := s.GetESignTokenFromESignServer()
    if err != nil {
        return "", err
    }
    token = eSignTokenResp.Token
    logx.Infof("从e签宝服务器获取token成功")

    // 写入缓存
    err = s.SetESignTokenToCacheData(token, eSignTokenResp.ExpiresIn)
    if err != nil {
        return token, err
    }
    logx.Infof("写入缓存token成功")

    return token, nil
}

// GetESignTokenFromESignServer 从e签宝服务器获取token
// https://open.esign.cn/doc/opendoc/identity_service/szr5s9
func (s *AuthService) GetESignTokenFromESignServer() (eSignTokenResp *types.GetESignTokenResponse, err error) {
	// 发起HTTP请求,获取e签宝的token
	requestUrl := s.config.BaseURL + api.GetESignTokenPath
	requestBody := &types.GetESignTokenRequest{
		AppId:     s.config.AppID,
		Secret:    s.config.AppSecret,
		GrantType: s.config.GrantType,
	}
	requestHeaders := map[string]string{
		"Content-Type": "application/json; charset=UTF-8",
	}
	response, err := utils.SendHttpPostRequest(requestUrl, requestBody, requestHeaders, s.config.IsWriteLog)
	if err != nil {
		return nil, err
	}

	// 解析响应体
	responseStruct, err := api.GetESignCommonResponse(response)
  // ...省略部分代码...
  
	return &responseDataStruct, nil
}

func (s *AuthService) GetESignTokenFromCacheData() (token string, err error) {
	token, err = s.config.RedisClient.Get(api.ESignAccessTokenKey)
	if err != nil {
		logx.Errorf("从缓存获取 token 失败: %v", err)
		return "", err
	}

	//调用e签宝接口检测Token是否有效,如果无效则重新获取 todo

	return token, nil
}

// 缓存设置逻辑
func (s *AuthService) SetESignTokenToCacheData(token string, eSignExpiresIn string) error {
    // 设置Redis缓存
    err = s.config.RedisClient.Set(api.ESignAccessTokenKey, token, expireDuration)
    if err != nil {
        logx.Errorf("设置缓存token失败: %v", err)
        return err
    }
    return nil
}

2. 模板管理功能

SDK支持合同模板的查询和使用,通过模板快速创建合同文件,提高了合同创建的效率和标准化程度。先根据e签宝的合同模板ID查询到具体的合同空模板信息,再填充进去合同挖空的数据,生成填充后的合同文件PDF。

// GetESignTemplateDetail 获取流程模版详情
// e签宝官方接口文档 https://open.esign.cn/doc/opendoc/file-and-template3/pfzut7ho9obc7c5r
func (s *TemplateService) GetESignTemplateDetail(eSignTemplateId string, queryComponents bool, writeLog bool) (eSignResponse *types.ESignCommonResponse, err error) {
    actionName := "请求e签宝获取流程模版详情"

    // 构建查询参数
    params := url.Values{}
    params.Add("orgId", s.config.OrgId)
    params.Add("signTemplateId", eSignTemplateId)
    if qc := strconv.FormatBool(queryComponents); qc != "" {
        params.Add("queryComponents", qc)
    }

    // 发起HTTP请求
    // ...省略部分代码...
    
    return eSignResponse, nil
}

// CreateByTemplate 通过模板创建文件
// e签宝官方接口文档 https://open.esign.cn/doc/opendoc/saas_api/cz9d65_sh823i?searchText=
func (s *TemplateService) CreateByTemplate(eSignTemplateDocFileId, eSignTemplateDocFileName string, simpleFormFields map[string]string, writeLog bool) (eSignResponse *types.ESignCommonResponse, err error) {
	actionName := "通过模板创建文件"

	requestUrl := s.config.BaseURL + api.CreateESignFileByTemplate
	requestBody := &types.CreateESignFileByTemplateRequest{
		TemplateDocFileId:   eSignTemplateDocFileId,
		TemplateDocFileName: eSignTemplateDocFileName,
		SimpleFormFields:    simpleFormFields,
	}
	requestHeaders, err := s.authService.RequestESignHeaders()
  
  // 发起HTTP请求
	// ...省略部分代码...
  
	return eSignResponse, nil
}

3. 一键发起签署流程

SDK封装了e签宝的复杂API调用,提供了简洁的接口,使开发者能够通过几行代码就能发起一个完整的签署流程。

// ESignCreateFlowOneStep 请求e签宝发起签署流程,此流程为e签宝自动给签署人发送短信
// e签宝官方接口文档 https://open.esign.cn/doc/opendoc/paas_api/pwd6l4
func (s *SignService) ESignCreateFlowOneStep(requestESignCreateFlowData *types.ESignCreateFlowRequestData, writeLog bool) (eSignResponse *types.ESignCommonResponse, err error) {
    //数据校验
    signerName := requestESignCreateFlowData.SignerName
    signerPhone := requestESignCreateFlowData.SignerPhone
    contractFiles := requestESignCreateFlowData.ContractFiles
    // ...省略部分代码...
    
    // 获取签署人账号ID
    signerAccountId, err := s.accountService.GetOrCreateESignSignerAccountId(signerName, signerPhone, false)
    if err != nil {
        return nil, err
    }
    
    // 构建签署请求
    // ...省略部分代码...
    
    // 发起HTTP请求并返回结果
    // ...省略部分代码...
    
    return eSignResponse, nil
}

4. 发起签署后的数据查询

发起签署后,可以查询签署链接、撤回签署流程(撤回后,签署流程中止,所有签署短信打开后无效)、获取签署完成后的文档链接。

// GetESignExecuteUrlByFlowId 查询签署链接
// e签宝官方接口文档 https://open.esign.cn/doc/opendoc/saas_api/fh3gh1_dwz08n
func (s *SignService) GetESignExecuteUrlByFlowId(flowId, signerName, signerPhone string, writeLog bool) (eSignResponse *types.ESignCommonResponse, err error) {
	actionName := "查询签署链接:"

	//数据校验
	if flowId == "" || signerName == "" || signerPhone == "" {
		return nil, errors.New(actionName + "传入的参数错误:flowId、签署人姓名、签署人手机号都不能为空")
	}

	//获取签署人账号
	signerAccountId, err := s.accountService.GetOrCreateESignSignerAccountId(signerName, signerPhone, false)
	if err != nil {
		return nil, err
	}
	if signerAccountId == "" {
		return nil, errors.New("签署人账号获取失败")
	}

	// 构建查询参数
	params := url.Values{}
	params.Add("accountId", signerAccountId)

	// 发起HTTP请求并返回结果
  // ...省略部分代码...
  
	return eSignResponse, nil
}

// ESignFlowRevoke PUT 撤回签署流程:撤销签署流程,撤销后流程中止,所有签署短信打开后无效。
// 文档地址: https://open.esign.cn/doc/opendoc/saas_api/hv1dii_uqoamg
func (s *SignService) ESignFlowRevoke(flowId string, writeLog bool) (eSignResponse *types.ESignCommonResponse, err error) {
	actionName := "撤回签署流程:"

	//数据校验
	if flowId == "" {
		return nil, errors.New(actionName + "传入的参数错误:flowId不能为空")
	}

	// 发起HTTP请求,这里需要PUT请求
	requestPath := strings.Replace(api.ESignFlowRevokePath, "{FLOW_ID}", flowId, 1) //替换 {FLOW_ID}
	requestUrl := s.config.BaseURL + requestPath
	requestHeaders, err := s.authService.RequestESignHeaders()
	
  // 发起HTTP请求并返回结果
  // ...省略部分代码...
  
	return eSignResponse, nil
}

// GetESignDocumentsUrlByFlowId 获取签署完成后的文档链接
// 文档地址: https://open.esign.cn/doc/opendoc/saas_api/oyqsoq_zknh6g
func (s *SignService) GetESignDocumentsUrlByFlowId(flowId string, writeLog bool) (eSignDocumentsDocs []types.GetDocumentsUrlResponseDataDocs, err error) {
	actionName := "查询签署完成后的文档链接:"

	//数据校验
	if flowId == "" {
		return nil, errors.New(actionName + "传入的参数错误:flowId不能为空")
	}

	// 发起HTTP请求
	requestPath := strings.Replace(api.GetESignDocumentsUrlByFlowId, "{FLOW_ID}", flowId, 1) //替换 {FLOW_ID}
	requestUrl := s.config.BaseURL + requestPath
	requestHeaders, err := s.authService.RequestESignHeaders()
  
	// 发起HTTP请求并返回结果
  // ...省略部分代码...

	// 解析Data结构
	documentsUrlResponseData := &types.GetDocumentsUrlResponseData{}
	err = utils.JsonUnmarshalToStruct(eSignResponse.Data, &documentsUrlResponseData)
	if err != nil {
		return nil, api.ParseESignResponseDataError(actionName, err)
	}
	return documentsUrlResponseData.Docs, nil
}

以上代码只是部分片段,完整代码参考文末的github仓库。

设计思想与特点

1. 接口分离原则

SDK严格遵循接口分离原则,每个服务都定义了对应的接口,使客户端只依赖于它需要的接口,而不是具体实现。这种设计使代码更加灵活,便于测试和扩展。

type AuthServiceInterface interface {
	GetESignToken(useCache bool) (token string, err error)
	GetESignTokenFromESignServer() (eSignTokenResp *types.GetESignTokenResponse, err error)
	GetESignTokenFromCacheData() (token string, err error)
	SetESignTokenToCacheData(token string, eSignExpiresIn string) error
}

var _ AuthServiceInterface = (*AuthService)(nil)

type AuthService struct {
  
}

2. 依赖注入模式

服务之间通过依赖注入的方式进行交互,而不是在服务内部直接创建依赖对象。这种模式降低了服务之间的耦合度,使代码更加易于测试和维护。

  • 控制反转:依赖关系的创建和管理从服务内部转移到外部
  • 松耦合:服务之间通过接口交互,而不是直接依赖具体实现
  • 可测试性:可以轻松注入mock对象进行单元测试
  • 可维护性:修改一个服务的实现不会影响其他服务
type Client struct {
	Auth     *auth_api.AuthService
	Template *template_api.TemplateService
	Account  *account_api.AccountService
	Sign     *sign_api.SignService
}

func NewClient(cfg *config.Config) *Client {
	// 初始化认证服务
	authService := auth_api.NewAuthService(cfg)
	// 构建并返回主客户端
	return &Client{
		Auth:     authService,
		Template: template_api.NewTemplateService(cfg, authService),
		Account:  account_api.NewAccountService(cfg, authService),
		Sign:     sign_api.NewSignService(cfg, authService),
	}
}

3. 缓存机制

SDK集成了Redis缓存功能,对频繁使用的认证信息进行缓存,减少了对e签宝API的请求次数,提高了SDK的性能。同时实现了缓存过期自动刷新机制,确保了数据的时效性。

func (s *AuthService) GetESignToken(useCache bool) (token string, err error) {
  // 从缓存中获取token
  token, err = s.GetESignTokenFromCacheData()
  if token != "" {
    return token, nil
  }

	// 从e签宝服务器获取token
	eSignTokenResp, err := s.GetESignTokenFromESignServer()
	token = eSignTokenResp.Token

	// 缓存token
	err = s.SetESignTokenToCacheData(token, eSignTokenResp.ExpiresIn)
	return token, nil
}

4. 环境变量配置

SDK支持从.env文件加载配置,使敏感信息(如AppID、AppSecret等)不直接硬编码在代码中,提高了配置的安全性和灵活性。

ESIGN_APP_ID=test
ESIGN_APP_SECRET=test
ESIGN_BASE_URL=https://smlopenapi.esign.cn

5. 错误处理

SDK采用了统一的错误处理机制,对API调用过程中可能出现的各种错误进行了封装和处理,为开发者提供了清晰的错误信息,便于问题定位和解决。

// 构建请求e签宝的headers失败的错误封装
func BuildRequestESignHeadersError(actionName string, err error) error {
	return errors.New(actionName + "构建请求e签宝的headers失败:" + err.Error())
}

// 发送http请求失败的错误封装
func SendHttpRequestError(actionName string, err error) error {
	return errors.New(actionName + "发送http请求失败:" + err.Error())
}

// 解析e签宝响应体失败的错误封装
func ParseESignResponseError(actionName string, err error) error {
	return errors.New(actionName + "解析e签宝响应体失败:" + err.Error())
}

// e签宝返回错误封装
func GetESignResponseError(eSignResponse *types.ESignCommonResponse) error {
	return errors.New(fmt.Sprintf("[e签宝返回错误]code:%d,message:%s", eSignResponse.Code, eSignResponse.Message))
}

// 解析e签宝响应体Data字段失败的错误封装
func ParseESignResponseDataError(actionName string, err error) error {
	return errors.New(actionName + "解析e签宝响应体Data字段失败:" + err.Error())
}

测试用例调试

在SDK中我已经封装好了测试用例文件,直接运行就可以快速调试。首先复制 .env.demo.env,然后配置好你的e签宝相关账户信息。这些信息可以找e签宝技术人员获取。

ESIGN_APP_ID=test
ESIGN_APP_SECRET=test
ESIGN_BASE_URL=https://smlopenapi.esign.cn
ESIGN_ORG_ID=test
ESIGN_GRANT_TYPE=client_credentials
IS_WRITE_LOG=true #是否写入HTTP请求的日志的全局开关

1. 测试获取e签宝token

代码 tests/auth_test.go

package tests

// TestGetToken 测试获取Token | go test tests/auth_test.go -v -run TestGetToken
func TestGetToken(t *testing.T) {
	testClient, err := initialize.NewTestClient()
	if err != nil {
		t.Errorf("创建测试客户端失败: %v\n", err)
		return
	}
	client := esign.NewClient(testClient.Conf)
	token, err := client.Auth.GetESignToken(false)
	if err != nil {
		t.Errorf("Failed to get token: %v\n", err)
		return
	}
	t.Logf("token: %v", token)
}

image-20251011143345911

2. 测试通过模板创建文件

代码 tests/template_test.go

package tests

// TestGetESignTemplateDetail 测试获取流程模版详情 | go test tests/template_test.go -v -run TestGetESignTemplateDetail
func TestGetESignTemplateDetail(t *testing.T) {
	testClient, err := initialize.NewTestClient()
	if err != nil {
		t.Errorf("创建测试客户端失败: %v\n", err)
		return
	}
	client := esign.NewClient(testClient.Conf)

	eSignTemplateId := "213fd9a0xxxxx" //流程模板ID
	queryComponents := true                               //是否需要查询控件信息:true-是 false-否(默认false)
	writeLog := false                                     //获取模板返回的数据量很大,因此可以根据情况考虑是否关闭写入日志
	templateDetail, err := client.Template.GetESignTemplateDetail(eSignTemplateId, queryComponents, writeLog)
	if err != nil {
		t.Errorf("Failed to get template detail: %v", err)
	}
	t.Logf("templateDetail: %v", utils.JsonMarshalNoEscape(templateDetail))
}

// TestCreateByTemplate 测试通过模板创建文件 | go test tests/template_test.go -v -run TestCreateByTemplate
func TestCreateByTemplate(t *testing.T) {
	testClient, err := initialize.NewTestClient()
	if err != nil {
		t.Errorf("创建测试客户端失败: %v\n", err)
		return
	}
	client := esign.NewClient(testClient.Conf)

	// 1. 获取模板信息
	eSignTemplateId := "213fd9a04xxxxx" //流程模板ID
	queryComponents := true                               //是否需要查询控件信息:true-是 false-否(默认false)
	writeLog := false                                     //获取模板返回的数据量很大,因此可以根据情况考虑是否关闭写入日志
	eSignResponseTemplateDetail, err := client.Template.GetESignTemplateDetail(eSignTemplateId, queryComponents, writeLog)
	if err != nil {
		t.Errorf("GetESignTemplateDetail error1: %v", err)
		return
	}
	//utils.LogxInfow(eSignResponseTemplateDetail, "eSignResponseTemplateDetail")

	// 2. 判断e签宝返回的code是否成功
	if eSignResponseTemplateDetail.Code != api.ESignResponseCodeSuccess {
		t.Errorf("GetESignTemplateDetail error2: %v", api.GetESignResponseError(eSignResponseTemplateDetail))
		return
	}

	// 3. 解析模板数据到结构体
	eSignTemplateData := types.GetESignTemplateDetailResponse{}
	err = utils.JsonUnmarshalToStruct(eSignResponseTemplateDetail.Data, &eSignTemplateData)
	if err != nil {
		t.Errorf("GetESignTemplateDetail JsonUnmarshalToStruct error: %v", err)
		return
	}

	// 4. 生成合同模板数据
	eSignTemplateDocFileId := eSignTemplateData.Docs[0].FileId
	eSignTemplateDocFileName := "rxESignGoSdkDemo-" + utils.GetCurrentTime() + "-" + eSignTemplateData.Docs[0].FileName //自定义生成后的模板文件名称 todo 改为你自己的格式
	eSignTemplateDocSimpleFormFields := make(map[string]string)
	for _, field := range eSignTemplateData.Participants {
		for _, component := range field.Components {
			//这里先简单粗暴的把控件的名称赋值给控件编码,正常业务场景下应该根据你自己的业务去赋值 todo
			eSignTemplateDocSimpleFormFields[component.ComponentKey] = component.ComponentName
		}
	}

	// 5. 请求e签宝创建合同模板数据CreateESignFileByTemplateRequest
	createByTemplateResponse, err := client.Template.CreateByTemplate(eSignTemplateDocFileId, eSignTemplateDocFileName, eSignTemplateDocSimpleFormFields, true)
	if err != nil {
		t.Errorf("CreateByTemplate error1: %v", err)
		return
	}
	// ...省略部分代码...

	// 6.将e签宝返回的downloadUrl上传到自己服务器或OSS todo
	createByTemplateResponseData.DownloadOssUrl = "todo_your_server_file_url"
	createByTemplateResponseData.TemplateId = eSignTemplateId

	utils.LogxInfow(createByTemplateResponseData, "createByTemplateResponseData")
}

image-20251011144246772

image-20251011144523671

查看生成的合同文件(签署前的合同文件),这里仅做调试和演示使用,先简单粗暴的把控件的名称赋值给控件编码,正常业务场景下应该根据你自己的业务逻辑去赋值。

image-20251011150035750

3. 测试发起签署流程

代码 tests/sign_test.go

package tests

var signerName = "张三"
var signerPhone = "1321234xxxx"

// TestESignCreateFlowOneStep 请求e签宝发起签署流程 | go test tests/sign_test.go -v -run TestESignCreateFlowOneStep
func TestESignCreateFlowOneStep(t *testing.T) {
	testClient, err := initialize.NewTestClient()
	if err != nil {
		t.Errorf("创建测试客户端失败: %v\n", err)
		return
	}
	client := esign.NewClient(testClient.Conf)

	//合同文件列表,通过 go test tests/template_test.go -v -run TestCreateByTemplate 获取
	contractFiles := make([]types.ESignCreateFlowFiles, 0)
	contractFiles = append(contractFiles, types.ESignCreateFlowFiles{
		TemplateId: "213fd9a04xxxxx",
		EFileId:    "14909f52dxxxxx",
	})
	contractFiles = append(contractFiles, types.ESignCreateFlowFiles{
		TemplateId: "213fd9a04xxxxx",
		EFileId:    "b3957c055xxxxx",
	})
	//设置请求参数
	requestESignCreateFlowData := &types.ESignCreateFlowRequestData{
		SignerName:    signerName,
		SignerPhone:   signerPhone,
		CompanySealID: "", //可以留空,将会使用默认的公司印章
		ContractFiles: contractFiles,
	}
	createFlowResponse, err := client.Sign.ESignCreateFlowOneStep(requestESignCreateFlowData, true)
	if err != nil {
		t.Errorf("Failed to create flow: %v", err)
	}
	//t.Logf("createFlowResponse: %v", utils.JsonMarshalNoEscape(createFlowResponse))

	createFlowResponseData := types.ESignCreateFlowResponseData{}
	err = utils.JsonUnmarshalToStruct(createFlowResponse.Data, &createFlowResponseData)
	if err != nil {
		t.Errorf("ESignCreateFlow JsonUnmarshalToStruct error: %v", err)
		return
	}

	//这里获取到的flowId需要保存起来,可以用来:1.查询签署链接; 2.撤回签署流程; 3.签署完成后查询合同文档;
	t.Logf("createFlowResponseData flowId: %v", createFlowResponseData.ESignFlowId)
}

image-20251011145618174

执行后,对应的手机号会收到短信:

image-20251011150645088

打开链接:

image-20251011150704649

然后直接去签署就可以了。

go项目中引入

1. 安装扩展和基础配置

我这里以go-zero框架为例演示如何在项目中引入这个SDK,关于go-zero框架的使用,可以参考这篇文章:《go-zero框架基本配置和错误码封装

首先使用 go get github.com/renxingcode/esign-go-sdk 安装扩展,然后在go-zero 的上下文中配置e签宝的SDK:

// internal/config/config.go
type Config struct {
	rest.RestConf
  
	//e签宝配置
	ESign struct {
		AppId      string
		AppSecret  string
		BaseUrl    string
		OrgID      string
		GrantType  string
		IsWriteLog string
	}
}

// internal/svc/service_context.go
type ServiceContext struct {
	Config   config.Config
	ESignSdk *esign.Client // e签宝的sdk对象
}

func NewServiceContext(c config.Config) *ServiceContext {
	// ... 省略其它代码

	//初始化e签宝客户端
	esignSdkConfig, err := eSignConfig.NewConfig(c.ESign.AppId, c.ESign.AppSecret, c.ESign.BaseUrl, c.ESign.OrgID, c.ESign.GrantType, c.ESign.IsWriteLog)
	if err != nil {
		logx.Errorf("Failed to init eSignSdk, err: %v", err)
	}
	esignSdk := esign.NewClient(esignSdkConfig)

	return &ServiceContext{
		Config: c,
		ESignSdk: esignSdk,
	}
}

在环境配置文件中配置e签宝需要的账号信息:

## etc/gozero-api-dev.yaml,以下数据改为你自己的账号信息
ESign:
  AppId: "test"
  AppSecret: "test"
  BaseUrl: "https://smlopenapi.esign.cn"
  OrgID: "test"
  GrantType: "client_credentials"
  IsWriteLog: "true"

2. 合同模板相关

internal/logic/eSignDemo/get_e_sign_template_detail_logic.go 中编写获取流程模板详情的逻辑:

package eSignDemo

import (
	"context"
	"github.com/renxingcode/esign-go-sdk"
	"github.com/zeromicro/go-zero/core/logx"
	"gozero-demo/internal/svc"
	"gozero-demo/internal/types"
)

type GetESignTemplateDetailLogic struct {
	logx.Logger
	ctx      context.Context
	svcCtx   *svc.ServiceContext
	eSignSdk *esign.Client
}

// 获取流程模版详情
func NewGetESignTemplateDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetESignTemplateDetailLogic {
	return &GetESignTemplateDetailLogic{
		Logger:   logx.WithContext(ctx),
		ctx:      ctx,
		svcCtx:   svcCtx,
		eSignSdk: svcCtx.ESignSdk,
	}
}

type GetESignTemplateDetailRequest struct {
	ESignTemplateId string `json:"e_sign_template_id"`      // e签宝模板ID
	WriteLog        bool   `json:"write_log,default=false"` // 是否记录日志
}

func (l *GetESignTemplateDetailLogic) GetESignTemplateDetail(req *types.GetESignTemplateDetailRequest) (resp *types.CommonResponse, err error) {
	templateDetail, err := l.eSignSdk.Template.GetESignTemplateDetail(req.ESignTemplateId, true, req.WriteLog)
	if err != nil {
		return nil, err
	}
	return types.SuccessResponse(templateDetail, l.ctx), nil
}

这里几乎不需要有任何逻辑,直接调用e签宝的SDK,获取数据。调试效果:

image-20251013145311709

获取到了合同模板和合同控件的数据之后,就可以 通过模板创建文件了,操作如下:

代码:internal/logic/eSignDemo/create_by_template_logic.go

type CreateByTemplateRequest struct {
	ESignTemplateId string `json:"e_sign_template_id"`      // e签宝模板ID
	WriteLog        bool   `json:"write_log,default=false"` // 是否记录日志
}

func (l *CreateByTemplateLogic) CreateByTemplate(req *types.CreateByTemplateRequest) (resp *types.CommonResponse, err error) {
	// 1. 获取模板信息
	templateDetail, err := l.eSignSdk.Template.GetESignTemplateDetail(req.ESignTemplateId, true, req.WriteLog)
	if err != nil {
		return nil, err
	}

	// 2. 判断e签宝返回的code是否成功
	if templateDetail.Code != api.ESignResponseCodeSuccess {
		return nil, api.GetESignResponseError(templateDetail)
	}

	// 3. 解析模板数据到结构体
	eSignTemplateData := eSignTypes.GetESignTemplateDetailResponse{}
	err = utils.JsonUnmarshalToStruct(templateDetail.Data, &eSignTemplateData)
	if err != nil {
		return nil, err
	}

	// 4. 生成合同模板数据
	eSignTemplateDocFileId := eSignTemplateData.Docs[0].FileId
	eSignTemplateDocFileName := "rxESignGoSdkDemo-" + utils.GetCurrentTime() + "-" + eSignTemplateData.Docs[0].FileName //自定义生成后的模板文件名称 todo 改为你自己的格式
	eSignTemplateDocSimpleFormFields := make(map[string]string)
	for _, field := range eSignTemplateData.Participants {
		for _, component := range field.Components {
			//这里先简单粗暴的把控件的名称赋值给控件编码,正常业务场景下应该根据你自己的业务去赋值 todo
			eSignTemplateDocSimpleFormFields[component.ComponentKey] = component.ComponentName
		}
	}

	// 5. 请求e签宝创建合同模板数据CreateESignFileByTemplateRequest
	createByTemplateResponse, err := l.eSignSdk.Template.CreateByTemplate(eSignTemplateDocFileId, eSignTemplateDocFileName, eSignTemplateDocSimpleFormFields, true)
	if err != nil {
		return nil, err
	}
	if createByTemplateResponse.Code != api.ESignResponseCodeSuccess {
		return nil, api.GetESignResponseError(createByTemplateResponse)
	}
	createByTemplateResponseData := eSignTypes.CreateESignFileByTemplateResponse{}
	err = utils.JsonUnmarshalToStruct(createByTemplateResponse.Data, &createByTemplateResponseData)
	if err != nil {
		return nil, err
	}

	// 6.将e签宝返回的downloadUrl上传到自己服务器或OSS todo
	createByTemplateResponseData.DownloadOssUrl = "todo_your_server_file_url"
	createByTemplateResponseData.TemplateId = req.ESignTemplateId

	return types.SuccessResponse(createByTemplateResponseData, l.ctx), nil
}

image-20251013150312628

查看生成的合同文件(签署前的合同文件),这里仅做调试和演示使用,先简单粗暴的把控件的名称赋值给控件编码,正常业务场景下应该根据你自己的业务去赋值。记住这个接口返回的templateIdfileId,下一步发起签署的时候要用到。多次生成的合同模板文件可以批量发起签署。

生成的合同文件大概效果如下:

image-20251011150035750

3. 发起签署流程

internal/logic/eSignDemo/create_flow_one_step_logic.go 中编写发起签署流程的逻辑:

type CreateFlowOneStepRequest struct {
	SignerName    string                          `json:"signer_name"`              // 签署人姓名
	SignerPhone   string                          `json:"signer_phone"`             // 签署人手机号
	CompanySealID string                          `json:"company_seal_id,optional"` // 公司印章ID,可以留空,将会使用默认的公司印章
	ContractFiles []CreateFlowOneStepContractFile `json:"contract_files"`           // 合同文件列表
	WriteLog      bool                            `json:"write_log,default=false"`  // 是否记录日志
}
type CreateFlowOneStepContractFile struct {
	TemplateId string `json:"template_id"` // 模板ID
	EFileId    string `json:"e_fileid"`    // e签宝文件ID
}

func (l *CreateFlowOneStepLogic) CreateFlowOneStep(req *types.CreateFlowOneStepRequest) (resp *types.CommonResponse, err error) {
	//合同文件列表,通过 /eSignDemo/CreateByTemplate 获取到返回的 templateId 和 fileId, 可能是多个,需要组装成数组形式
	contractFiles := make([]eSignTypes.ESignCreateFlowFiles, 0)
	for _, contractFile := range req.ContractFiles {
		contractFiles = append(contractFiles, eSignTypes.ESignCreateFlowFiles{
			TemplateId: contractFile.TemplateId,
			EFileId:    contractFile.EFileId,
		})
	}

	//设置请求参数
	requestESignCreateFlowData := &eSignTypes.ESignCreateFlowRequestData{
		SignerName:    req.SignerName,
		SignerPhone:   req.SignerPhone,
		CompanySealID: "", //可以留空,将会使用默认的公司印章
		ContractFiles: contractFiles,
	}
	createFlowResponse, err := l.eSignSdk.Sign.ESignCreateFlowOneStep(requestESignCreateFlowData, true)
	if err != nil {
		return nil, err
	}

	createFlowResponseData := eSignTypes.ESignCreateFlowResponseData{}
	err = utils.JsonUnmarshalToStruct(createFlowResponse.Data, &createFlowResponseData)
	if err != nil {
		return nil, err
	}

	//这里获取到的flowId需要保存起来,可以用来:1.查询签署链接; 2.撤回签署流程; 3.签署完成后查询合同文档;
	fmt.Printf("createFlowResponseData flowId: %v", createFlowResponseData.ESignFlowId)

	return types.SuccessResponse(createFlowResponseData, l.ctx), nil
}

image-20251013155828291

至此,通过e签宝SDK实现e签宝合同模板的获取和签署流程就基本走通了。在实际开发过程中,需要对每一步的数据记录到数据库留存,以及一些细节部分需要加上容错处理,这里只是简单演示一下这个SDK的基本用法。

在go-zero框架中引入e签宝SDK示例的源代码:https://gitee.com/rxbook/gozero-demo

持续迭代和发布

后续开发迭代之后,可以通过我内置的脚本快速发布。代码:scripts/git-push-set-tag.sh

# 说明: Git提交当前分支并设置tag
# 运行方式: sh git-push-set-tag.sh 提交代码的备注信息 tag名称

if [ ! -n "$1" ] ;then
    # mark="修改"
    echo "参数1:备注不能为空"
    exit 2
else
    mark=$1
fi

if [ ! -n "$2" ] ;then
    # 获取最新的 tag
    latest_tag=$(git tag --sort=-creatordate | head -n 1)
    if [ -n "$latest_tag" ]; then
        echo "参数2:tag名称不能为空,当前最新tag: $latest_tag"
    else
        echo "参数2:tag名称不能为空,当前没有可用的tag,你可以使用类似 v0.0.1 的格式作为第一个tag"
    fi
    exit 2
else
    tag=$2
fi

git pull
git add .
git commit -m "$mark"
git push

git tag -a "$tag" -m "new tag $tag"
git push origin "$tag"

运行试试:

image-20251011161140057

这样,一条命令就可以提交代码改动,并创建新的Tag,让调用者执行 go get github.com/renxingcode/esign-go-sdk 就可以更新了。

如果你只想快速提交代码并push到远程分支,并不想创建新的Tag,可以查看 scripts/git-push.sh 脚本。

总结

本项目 esign-go-sdk 通过精心的架构设计和智能的缓存策略,为Go语言开发者提供了高效、稳定的e签宝API集成方案。其模块化设计不仅便于维护和测试,也为未来的功能扩展留下了充足的空间。你也可以对此项目复制和二次开发,创建适合自己的业务SDK,可以自行学习和研究,请不要用于商业用途。

核心价值

  • 降低集成复杂度,提升开发效率
  • 智能缓存减少API调用次数,提升性能
  • 完善的错误处理和日志记录
  • 良好的可扩展性和维护性

项目源代码地址在我的github仓库:https://github.com/renxingcode/esign-go-sdk,欢迎fork,如果有使用上的问题,可以给我留言。欢迎提供宝贵意见,大家一起学习和进步!

本文版权归 CSDN@码农兴哥 所有,未经允许请勿转载,翻版必究!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农兴哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值