文章目录
项目概述
在企业级应用开发中,电子签名服务已经成为不可或缺的一部分,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)
}
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")
}
查看生成的合同文件(签署前的合同文件),这里仅做调试和演示使用,先简单粗暴的把控件的名称赋值给控件编码,正常业务场景下应该根据你自己的业务逻辑去赋值。
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)
}
执行后,对应的手机号会收到短信:
打开链接:
然后直接去签署就可以了。
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,获取数据。调试效果:
获取到了合同模板和合同控件的数据之后,就可以 通过模板创建文件
了,操作如下:
代码: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
}
查看生成的合同文件(签署前的合同文件),这里仅做调试和演示使用,先简单粗暴的把控件的名称赋值给控件编码,正常业务场景下应该根据你自己的业务去赋值。记住这个接口返回的templateId
和fileId
,下一步发起签署的时候要用到。多次生成的合同模板文件可以批量发起签署。
生成的合同文件大概效果如下:
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
}
至此,通过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"
运行试试:
这样,一条命令就可以提交代码改动,并创建新的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@码农兴哥 所有,未经允许请勿转载,翻版必究!