This commit is contained in:
2025-07-28 01:46:39 +08:00
parent b03129667a
commit 357639462a
219 changed files with 21634 additions and 8138 deletions

View File

@@ -2,18 +2,43 @@ package esign
import "fmt"
type EsignContractConfig struct {
Name string `json:"name" yaml:"name"`
ExpireDays int `json:"expireDays" yaml:"expire_days"`
RetryCount int `json:"retryCount" yaml:"retry_count"`
}
type EsignAuthConfig struct {
OrgAuthModes []string `json:"orgAuthModes" yaml:"org_auth_modes"`
DefaultAuthMode string `json:"defaultAuthMode" yaml:"default_auth_mode"`
PsnAuthModes []string `json:"psnAuthModes" yaml:"psn_auth_modes"`
WillingnessAuthModes []string `json:"willingnessAuthModes" yaml:"willingness_auth_modes"`
RedirectUrl string `json:"redirectUrl" yaml:"redirect_url"`
}
type EsignSignConfig struct {
AutoFinish bool `json:"autoFinish" yaml:"auto_finish"`
SignFieldStyle int `json:"signFieldStyle" yaml:"sign_field_style"`
ClientType string `json:"clientType" yaml:"client_type"`
RedirectUrl string `json:"redirectUrl" yaml:"redirect_url"`
}
// Config e签宝服务配置结构体
// 包含应用ID、密钥、服务器URL和模板ID等基础配置信息
// 新增Contract、Auth、Sign配置
type Config struct {
AppID string `json:"appId"` // 应用ID
AppSecret string `json:"appSecret"` // 应用密钥
ServerURL string `json:"serverUrl"` // 服务器URL
TemplateID string `json:"templateId"` // 模板ID
AppID string `json:"appId" yaml:"app_id"`
AppSecret string `json:"appSecret" yaml:"app_secret"`
ServerURL string `json:"serverUrl" yaml:"server_url"`
TemplateID string `json:"templateId" yaml:"template_id"`
Contract *EsignContractConfig `json:"contract" yaml:"contract"`
Auth *EsignAuthConfig `json:"auth" yaml:"auth"`
Sign *EsignSignConfig `json:"sign" yaml:"sign"`
}
// NewConfig 创建新的配置实例
// 提供配置验证和默认值设置
func NewConfig(appID, appSecret, serverURL, templateID string) (*Config, error) {
func NewConfig(appID, appSecret, serverURL, templateID string, contract *EsignContractConfig, auth *EsignAuthConfig, sign *EsignSignConfig) (*Config, error) {
if appID == "" {
return nil, fmt.Errorf("应用ID不能为空")
}
@@ -26,12 +51,15 @@ func NewConfig(appID, appSecret, serverURL, templateID string) (*Config, error)
if templateID == "" {
return nil, fmt.Errorf("模板ID不能为空")
}
return &Config{
AppID: appID,
AppSecret: appSecret,
ServerURL: serverURL,
TemplateID: templateID,
Contract: contract,
Auth: auth,
Sign: sign,
}, nil
}
@@ -60,7 +88,7 @@ const (
AuthModeBank = "PSN_BANK" // 银行卡认证
// 意愿认证模式
WillingnessAuthSMS = "CODE_SMS" // 短信验证码
WillingnessAuthSMS = "CODE_SMS" // 短信验证码
WillingnessAuthEmail = "CODE_EMAIL" // 邮箱验证码
// 证件类型常量
@@ -69,7 +97,7 @@ const (
// 签署区样式常量
SignFieldStyleNormal = 1 // 普通签章
SignFieldStyleSeam = 2 // 骑缝签章
SignFieldStyleSeam = 2 // 骑缝签章
// 签署人类型常量
SignerTypePerson = 0 // 个人
@@ -80,4 +108,4 @@ const (
// 客户端类型常量
ClientTypeAll = "ALL" // 所有客户端
)
)

View File

@@ -13,6 +13,24 @@ func Example() {
"your_app_secret",
"https://smlopenapi.esign.cn",
"your_template_id",
&EsignContractConfig{
Name: "测试合同",
ExpireDays: 30,
RetryCount: 3,
},
&EsignAuthConfig{
OrgAuthModes: []string{"ORG"},
DefaultAuthMode: "ORG",
PsnAuthModes: []string{"PSN"},
WillingnessAuthModes: []string{"WILLINGNESS"},
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
},
&EsignSignConfig{
AutoFinish: true,
SignFieldStyle: 1,
ClientType: "ALL",
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
},
)
if err != nil {
log.Fatal("配置创建失败:", err)
@@ -122,7 +140,22 @@ func Example() {
// ExampleBasicUsage 基础用法示例
func ExampleBasicUsage() {
// 最简单的用法 - 一行代码完成合同签署
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id", &EsignContractConfig{
Name: "测试合同",
ExpireDays: 30,
RetryCount: 3,
}, &EsignAuthConfig{
OrgAuthModes: []string{"ORG"},
DefaultAuthMode: "ORG",
PsnAuthModes: []string{"PSN"},
WillingnessAuthModes: []string{"WILLINGNESS"},
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
}, &EsignSignConfig{
AutoFinish: true,
SignFieldStyle: 1,
ClientType: "ALL",
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
})
client := NewClient(config)
// 快速合同签署
@@ -143,7 +176,22 @@ func ExampleBasicUsage() {
// ExampleWithCustomData 自定义数据示例
func ExampleWithCustomData() {
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id", &EsignContractConfig{
Name: "测试合同",
ExpireDays: 30,
RetryCount: 3,
}, &EsignAuthConfig{
OrgAuthModes: []string{"ORG"},
DefaultAuthMode: "ORG",
PsnAuthModes: []string{"PSN"},
WillingnessAuthModes: []string{"WILLINGNESS"},
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
}, &EsignSignConfig{
AutoFinish: true,
SignFieldStyle: 1,
ClientType: "ALL",
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
})
client := NewClient(config)
// 使用自定义模板数据
@@ -171,7 +219,22 @@ func ExampleWithCustomData() {
// ExampleEnterpriseAuth 企业认证示例
func ExampleEnterpriseAuth() {
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id")
config, _ := NewConfig("app_id", "app_secret", "server_url", "template_id", &EsignContractConfig{
Name: "测试合同",
ExpireDays: 30,
RetryCount: 3,
}, &EsignAuthConfig{
OrgAuthModes: []string{"ORG"},
DefaultAuthMode: "ORG",
PsnAuthModes: []string{"PSN"},
WillingnessAuthModes: []string{"WILLINGNESS"},
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
}, &EsignSignConfig{
AutoFinish: true,
SignFieldStyle: 1,
ClientType: "ALL",
RedirectUrl: "https://www.tianyuanapi.com/certification/callback",
})
client := NewClient(config)
// 企业认证

View File

@@ -34,8 +34,8 @@ func (s *FileOpsService) UpdateConfig(config *Config) {
func (s *FileOpsService) DownloadSignedFile(signFlowId string) (*DownloadSignedFileResponse, error) {
fmt.Println("开始下载已签署文件及附属材料...")
// 发送API请求
urlPath := fmt.Sprintf("/v3/sign-flow/%s/attachments", signFlowId)
// 按照最新e签宝文档接口路径应为 /v3/sign-flow/{signFlowId}/file-download-url
urlPath := fmt.Sprintf("/v3/sign-flow/%s/file-download-url", signFlowId)
responseBody, err := s.httpClient.Request("GET", urlPath, nil)
if err != nil {
return nil, fmt.Errorf("下载已签署文件失败: %v", err)

View File

@@ -44,8 +44,8 @@ func (h *HTTPClient) UpdateConfig(config *Config) {
func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error) {
// 生成签名所需参数
timestamp := getCurrentTimestamp()
nonce := generateNonce()
date := getCurrentDate()
// date := getCurrentDate()
date := ""
// 计算Content-MD5
contentMD5 := ""
@@ -53,14 +53,14 @@ func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error
contentMD5 = getContentMD5(body)
}
// 根据Java示例Headers为空字符串
headers := ""
// 生成签名
// 生成签名用原始urlPath
signature := generateSignature(h.config.AppSecret, method, "*/*", contentMD5, "application/json", date, headers, urlPath)
// 创建HTTP请求
url := h.config.ServerURL + urlPath
// 实际请求url用encode后的urlPath
encodedURLPath := encodeURLQueryParams(urlPath)
url := h.config.ServerURL + encodedURLPath
req, err := http.NewRequest(method, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("创建HTTP请求失败: %v", err)
@@ -69,12 +69,11 @@ func (h *HTTPClient) Request(method, urlPath string, body []byte) ([]byte, error
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-MD5", contentMD5)
req.Header.Set("Date", date)
// req.Header.Set("Date", date)
req.Header.Set("Accept", "*/*")
req.Header.Set("X-Tsign-Open-App-Id", h.config.AppID)
req.Header.Set("X-Tsign-Open-Auth-Mode", "Signature")
req.Header.Set("X-Tsign-Open-Ca-Timestamp", timestamp)
req.Header.Set("X-Tsign-Open-Nonce", nonce)
req.Header.Set("X-Tsign-Open-Ca-Signature", signature)
// 发送请求
@@ -197,3 +196,24 @@ func sortURLQueryParams(urlPath string) string {
}
return basePath
}
// encodeURLQueryParams 对urlPath中的query参数值进行encode
func encodeURLQueryParams(urlPath string) string {
if !strings.Contains(urlPath, "?") {
return urlPath
}
parts := strings.SplitN(urlPath, "?", 2)
basePath := parts[0]
queryString := parts[1]
values, err := url.ParseQuery(queryString)
if err != nil {
return urlPath
}
var encodedPairs []string
for key, vals := range values {
for _, val := range vals {
encodedPairs = append(encodedPairs, key+"="+url.QueryEscape(val))
}
}
return basePath + "?" + strings.Join(encodedPairs, "&")
}

View File

@@ -66,6 +66,9 @@ func (s *OrgAuthService) GetAuthURL(req *OrgAuthRequest) (string, string, string
},
},
ClientType: ClientTypeAll,
RedirectConfig: &RedirectConfig{
RedirectUrl: s.config.Auth.RedirectUrl,
},
}
// 序列化请求数据

View File

@@ -43,7 +43,7 @@ func (s *SignFlowService) Create(req *CreateSignFlowRequest) (string, error) {
Docs: []DocInfo{
{
FileId: req.FileID,
FileName: "天远数据API合作协议.pdf",
FileName: s.config.Contract.Name,
},
},
SignFlowConfig: s.buildSignFlowConfig(),
@@ -141,9 +141,9 @@ func (s *SignFlowService) buildPartyASigner(fileID string) SignerInfo {
AutoSign: true,
SignFieldStyle: SignFieldStyleNormal,
SignFieldPosition: &SignFieldPosition{
PositionPage: "1",
PositionPage: "8",
PositionX: 200,
PositionY: 200,
PositionY: 430,
},
},
},
@@ -188,9 +188,9 @@ func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, t
AutoSign: false,
SignFieldStyle: SignFieldStyleNormal,
SignFieldPosition: &SignFieldPosition{
PositionPage: "1",
PositionX: 458,
PositionY: 200,
PositionPage: "8",
PositionX: 450,
PositionY: 430,
},
},
},
@@ -201,9 +201,9 @@ func (s *SignFlowService) buildPartyBSigner(fileID, signerAccount, signerName, t
// buildSignFlowConfig 构建签署流程配置
func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig {
return SignFlowConfig{
SignFlowTitle: "天远数据API合作协议签署",
SignFlowExpireTime: calculateExpireTime(7), // 7天后过期
AutoFinish: true, // 所有签署方完成后自动完结
SignFlowTitle: s.config.Contract.Name,
SignFlowExpireTime: calculateExpireTime(s.config.Contract.ExpireDays),
AutoFinish: s.config.Sign.AutoFinish,
AuthConfig: &AuthConfig{
PsnAvailableAuthModes: []string{AuthModeMobile3},
WillingnessAuthModes: []string{WillingnessAuthSMS},
@@ -211,5 +211,8 @@ func (s *SignFlowService) buildSignFlowConfig() SignFlowConfig {
ContractConfig: &ContractConfig{
AllowToRescind: false,
},
RedirectConfig: &RedirectConfig{
RedirectUrl: s.config.Sign.RedirectUrl,
},
}
}

View File

@@ -36,7 +36,7 @@ func (s *TemplateService) Fill(components []Component) (*FillTemplate, error) {
fmt.Println("开始填写模板生成文件...")
// 生成带时间戳的文件名
fileName := generateFileName("天远数据API合作协议", "pdf")
fileName := generateFileName(s.config.Contract.Name, "pdf")
// 构建请求数据
requestData := FillTemplateRequest{

View File

@@ -5,10 +5,40 @@ import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
// appendSignDataString 拼接待签名字符串
func appendSignDataString(httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters string) string {
if accept == "" {
accept = "*/*"
}
if contentType == "" {
contentType = "application/json; charset=UTF-8"
}
// 前四项
signStr := httpMethod + "\n" + accept + "\n" + contentMD5 + "\n" + contentType + "\n"
// 处理 date
if date == "" {
signStr += "\n"
} else {
signStr += date + "\n"
}
// 处理 headers
if headers == "" {
signStr += pathAndParameters
} else {
signStr += headers + "\n" + pathAndParameters
}
return signStr
}
// generateSignature 生成e签宝API请求签名
// 使用HMAC-SHA256算法对请求参数进行签名
//
@@ -24,8 +54,8 @@ import (
//
// 返回: Base64编码的签名字符串
func generateSignature(appSecret, httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters string) string {
// 构建待签名字符串按照e签宝API规范拼接
signStr := httpMethod + "\n" + accept + "\n" + contentMD5 + "\n" + contentType + "\n" + date + "\n" + headers + pathAndParameters
// 构建待签名字符串按照e签宝API规范拼接兼容Python实现细节
signStr := appendSignDataString(httpMethod, accept, contentMD5, contentType, date, headers, pathAndParameters)
// 使用HMAC-SHA256计算签名
h := hmac.New(sha256.New, []byte(appSecret))
@@ -102,3 +132,66 @@ func generateFileName(baseName, extension string) string {
func calculateExpireTime(days int) int64 {
return time.Now().AddDate(0, 0, days).UnixMilli()
}
// verifySignature 验证e签宝回调签名
func VerifySignature(callbackData interface{}, headers map[string]string, queryParams map[string]string, appSecret string) error {
// 1. 获取签名相关参数
signature, ok := headers["X-Tsign-Open-Signature"]
if !ok {
return fmt.Errorf("缺少签名头: X-Tsign-Open-Signature")
}
timestamp, ok := headers["X-Tsign-Open-Timestamp"]
if !ok {
return fmt.Errorf("缺少时间戳头: X-Tsign-Open-Timestamp")
}
// 2. 构建查询参数字符串
var queryKeys []string
for key := range queryParams {
queryKeys = append(queryKeys, key)
}
sort.Strings(queryKeys) // 按ASCII码升序排序
var queryValues []string
for _, key := range queryKeys {
queryValues = append(queryValues, queryParams[key])
}
queryString := strings.Join(queryValues, "")
// 3. 获取请求体数据
bodyData, err := getRequestBodyString(callbackData)
if err != nil {
return fmt.Errorf("获取请求体数据失败: %w", err)
}
// 4. 构建验签数据
data := timestamp + queryString + bodyData
// 5. 计算签名
expectedSignature := calculateSignature(data, appSecret)
// 6. 比较签名
if strings.ToLower(expectedSignature) != strings.ToLower(signature) {
return fmt.Errorf("签名验证失败")
}
return nil
}
// calculateSignature 计算HMAC-SHA256签名
func calculateSignature(data, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(data))
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
}
// getRequestBodyString 获取请求体字符串
func getRequestBodyString(callbackData interface{}) (string, error) {
// 将map转换为JSON字符串
jsonBytes, err := json.Marshal(callbackData)
if err != nil {
return "", fmt.Errorf("JSON序列化失败: %w", err)
}
return string(jsonBytes), nil
}