418 lines
13 KiB
Go
418 lines
13 KiB
Go
package fadada
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// CreateSignFlow 基于签署任务模板创建签署任务(POST /sign-task/create-with-template)
|
||
// - autoStart:无填单时创建即自动提交;有填单时创建后填控件再 Start(完成自动提交)
|
||
// - 签署顺序:甲方 orderNo=1 先签(短信/验证);乙方 orderNo=2 后签,由系统按模板免验证签自动盖章
|
||
// - 乙方参与方开启免验证签(requestVerifyFree + freeSignType=template)
|
||
func (c *Client) CreateSignFlow(req *CreateSignFlowRequest) (*CreateSignFlowResult, error) {
|
||
if err := validateCreateSignFlowRequest(req); err != nil {
|
||
return nil, err
|
||
}
|
||
if c.config.OpenCorpID == "" {
|
||
return nil, fmt.Errorf("法大大 open_corp_id 不能为空")
|
||
}
|
||
if c.config.TemplateID == "" {
|
||
return nil, fmt.Errorf("法大大 template_id 不能为空")
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
needFill := req.Fill != nil
|
||
// 有填单须先填再提交,创建时不能 autoStart;无填单则创建时自动提交
|
||
autoStart := !needFill
|
||
|
||
subject := strings.TrimSpace(req.Subject)
|
||
if subject == "" {
|
||
if c.config.Contract != nil && c.config.Contract.Name != "" {
|
||
subject = c.config.Contract.Name
|
||
} else {
|
||
subject = "海宇数据-合作协议"
|
||
}
|
||
if req.PartyAName != "" {
|
||
subject = fmt.Sprintf("%s-%s", subject, req.PartyAName)
|
||
}
|
||
}
|
||
|
||
actors := c.buildSignActors(req)
|
||
expiresTime := c.buildExpiresTime()
|
||
|
||
res, err := c.http.PostBiz(pathCreateSignTaskTemplate, accessToken, &createSignTaskWithTemplateReq{
|
||
SignTaskSubject: subject,
|
||
Initiator: &openID{IDType: "corp", OpenID: c.config.OpenCorpID},
|
||
SignTemplateID: c.config.TemplateID,
|
||
AutoStart: autoStart,
|
||
AutoFillFinalize: true,
|
||
AutoFinish: true,
|
||
SignInOrder: true,
|
||
ExpiresTime: expiresTime,
|
||
FreeSignType: freeSignTypeTemplate, // 按模板免验证签,无需 businessId
|
||
TransReferenceID: strings.TrimSpace(req.TransReferenceID),
|
||
Actors: actors,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, fmt.Errorf("基于模板创建法大大签署任务失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
var data createSignTaskData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析创建签署任务响应失败: %w", err)
|
||
}
|
||
signTaskID := data.SignTaskID
|
||
if signTaskID == "" {
|
||
return nil, fmt.Errorf("创建法大大签署任务失败: signTaskId 为空")
|
||
}
|
||
|
||
// 填单后自动提交:乙方免验证签自动盖章,甲方再取签署链接
|
||
if needFill {
|
||
fillReq := *req.Fill
|
||
if fillReq.CompanyName == "" {
|
||
fillReq.CompanyName = req.PartyAName
|
||
}
|
||
if fillReq.UnifiedSocialCode == "" {
|
||
fillReq.UnifiedSocialCode = req.PartyAUSCC
|
||
}
|
||
if fillReq.AuthorizedRepName == "" {
|
||
fillReq.AuthorizedRepName = req.TransactorName
|
||
}
|
||
if err := c.FillSignTaskFields(signTaskID, &fillReq); err != nil {
|
||
return nil, err
|
||
}
|
||
if err := c.StartSignTask(signTaskID); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return &CreateSignFlowResult{SignTaskID: signTaskID}, nil
|
||
}
|
||
|
||
// StartSignTask 提交签署任务(POST /sign-task/start)
|
||
func (c *Client) StartSignTask(signTaskID string) error {
|
||
signTaskID = strings.TrimSpace(signTaskID)
|
||
if signTaskID == "" {
|
||
return fmt.Errorf("signTaskId 不能为空")
|
||
}
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
res, err := c.http.PostBiz(pathStartSignTask, accessToken, &startSignTaskReq{SignTaskID: signTaskID})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if res.Code != successCode {
|
||
return fmt.Errorf("提交法大大签署任务失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetSignURL 获取参与方签署链接(POST /sign-task/actor/get-url)
|
||
func (c *Client) GetSignURL(signTaskID, actorID string) (*SignURLResult, error) {
|
||
signTaskID = strings.TrimSpace(signTaskID)
|
||
if signTaskID == "" {
|
||
return nil, fmt.Errorf("signTaskId 不能为空")
|
||
}
|
||
if actorID == "" {
|
||
actorID = c.config.ResolvePartyAActorID()
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
redirectURL := ""
|
||
if c.config.Sign != nil {
|
||
redirectURL = encodeRedirectURL(c.config.Sign.RedirectURL)
|
||
}
|
||
|
||
res, err := c.http.PostBiz(pathGetSignTaskActorURL, accessToken, &getActorURLReq{
|
||
SignTaskID: signTaskID,
|
||
ActorID: actorID,
|
||
RedirectURL: redirectURL,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, fmt.Errorf("获取法大大签署链接失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
|
||
var data getActorURLData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析签署链接响应失败: %w", err)
|
||
}
|
||
if data.ActorSignTaskURL == "" && data.ActorSignTaskEmbedURL == "" {
|
||
return nil, fmt.Errorf("获取法大大签署链接失败: url 为空 requestId=%s", res.RequestID)
|
||
}
|
||
|
||
return &SignURLResult{
|
||
SignURL: data.ActorSignTaskURL,
|
||
EmbedURL: data.ActorSignTaskEmbedURL,
|
||
ActorID: actorID,
|
||
SignTaskID: signTaskID,
|
||
}, nil
|
||
}
|
||
|
||
// GetSignTaskPreviewURL 获取签署任务预览链接(POST /sign-task/get-preview-url,约 2 小时/单次)
|
||
func (c *Client) GetSignTaskPreviewURL(signTaskID string) (*SignTaskPreviewURLResult, error) {
|
||
signTaskID = strings.TrimSpace(signTaskID)
|
||
if signTaskID == "" {
|
||
return nil, fmt.Errorf("signTaskId 不能为空")
|
||
}
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
redirectURL := ""
|
||
if c.config.Sign != nil {
|
||
redirectURL = encodeRedirectURL(c.config.Sign.RedirectURL)
|
||
}
|
||
res, err := c.http.PostBiz(pathGetSignTaskPreviewURL, accessToken, &getSignTaskPreviewURLReq{
|
||
SignTaskID: signTaskID,
|
||
RedirectURL: redirectURL,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, fmt.Errorf("获取法大大预览链接失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
var data getSignTaskPreviewURLData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析预览链接响应失败: %w", err)
|
||
}
|
||
previewURL := strings.TrimSpace(data.SignTaskPreviewURL)
|
||
if previewURL == "" {
|
||
return nil, fmt.Errorf("获取法大大预览链接失败: signTaskPreviewUrl 为空 requestId=%s", res.RequestID)
|
||
}
|
||
return &SignTaskPreviewURLResult{
|
||
SignTaskID: signTaskID,
|
||
PreviewURL: previewURL,
|
||
}, nil
|
||
}
|
||
|
||
// QuerySignStatus 查询签署任务状态(POST /sign-task/app/get-detail)
|
||
func (c *Client) QuerySignStatus(signTaskID string) (*SignStatusResult, error) {
|
||
signTaskID = strings.TrimSpace(signTaskID)
|
||
if signTaskID == "" {
|
||
return nil, fmt.Errorf("signTaskId 不能为空")
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
res, err := c.http.PostBiz(pathGetSignTaskDetail, accessToken, &getSignTaskDetailReq{
|
||
SignTaskID: signTaskID,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, fmt.Errorf("查询法大大签署状态失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
|
||
var data getSignTaskDetailData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析签署状态响应失败: %w", err)
|
||
}
|
||
|
||
status := data.SignTaskStatus
|
||
return &SignStatusResult{
|
||
SignTaskID: signTaskID,
|
||
SignTaskStatus: status,
|
||
Completed: IsSignTaskCompletedStatus(status),
|
||
Terminated: status == SignTaskStatusTerminated,
|
||
FinishTime: data.FinishTime,
|
||
TerminationNote: data.TerminationNote,
|
||
RevokeReason: data.RevokeReason,
|
||
}, nil
|
||
}
|
||
|
||
// IsSignTaskCompleted 签署任务是否已完成(可下载)
|
||
func (c *Client) IsSignTaskCompleted(signTaskID string) (bool, error) {
|
||
status, err := c.QuerySignStatus(signTaskID)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return status.Completed, nil
|
||
}
|
||
|
||
// IsSignTaskCompletedStatus 状态是否视为签署完成
|
||
func IsSignTaskCompletedStatus(status string) bool {
|
||
switch status {
|
||
case SignTaskStatusFinished, SignTaskStatusSignCompleted:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// DownloadSignedFiles 获取签署完成文件下载地址(POST /sign-task/owner/get-download-url)
|
||
func (c *Client) DownloadSignedFiles(signTaskID string) (*DownloadSignedFilesResult, error) {
|
||
signTaskID = strings.TrimSpace(signTaskID)
|
||
if signTaskID == "" {
|
||
return nil, fmt.Errorf("signTaskId 不能为空")
|
||
}
|
||
if c.config.OpenCorpID == "" {
|
||
return nil, fmt.Errorf("法大大 open_corp_id 不能为空")
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
res, err := c.http.PostBiz(pathGetOwnerSignTaskDownload, accessToken, &getOwnerDownloadURLReq{
|
||
OwnerID: &openID{IDType: "corp", OpenID: c.config.OpenCorpID},
|
||
SignTaskID: signTaskID,
|
||
FileType: "doc",
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, fmt.Errorf("获取法大大已签文件下载地址失败: code=%s msg=%s requestId=%s", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
|
||
var data getOwnerDownloadURLData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析下载地址响应失败: %w", err)
|
||
}
|
||
if data.DownloadURL == "" {
|
||
return nil, fmt.Errorf("获取法大大已签文件下载地址失败: downloadUrl 为空 requestId=%s", res.RequestID)
|
||
}
|
||
|
||
return &DownloadSignedFilesResult{
|
||
Files: []*SignedFile{{
|
||
DownloadURL: data.DownloadURL,
|
||
DownloadID: data.DownloadID,
|
||
}},
|
||
}, nil
|
||
}
|
||
|
||
// GenerateContractSigning 一站式:创建签署任务(可带填单)→ 获取甲方签署链接
|
||
func (c *Client) GenerateContractSigning(req *ContractSigningRequest) (*ContractSigningResult, error) {
|
||
if req == nil {
|
||
return nil, fmt.Errorf("签署请求不能为空")
|
||
}
|
||
createReq := req.CreateSignFlowRequest
|
||
createReq.Fill = &req.Fill
|
||
if createReq.PartyAName == "" {
|
||
createReq.PartyAName = req.Fill.CompanyName
|
||
}
|
||
if createReq.PartyAUSCC == "" {
|
||
createReq.PartyAUSCC = req.Fill.UnifiedSocialCode
|
||
}
|
||
if createReq.TransactorName == "" {
|
||
createReq.TransactorName = req.Fill.AuthorizedRepName
|
||
}
|
||
|
||
createRes, err := c.CreateSignFlow(&createReq)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
urlRes, err := c.GetSignURL(createRes.SignTaskID, c.config.ResolvePartyAActorID())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &ContractSigningResult{
|
||
SignTaskID: createRes.SignTaskID,
|
||
SignURL: firstNonEmpty(urlRes.EmbedURL, urlRes.SignURL),
|
||
EmbedURL: urlRes.EmbedURL,
|
||
}, nil
|
||
}
|
||
|
||
const freeSignTypeTemplate = "template" // 根据模板免验证签(无需 businessId)
|
||
|
||
func (c *Client) buildSignActors(req *CreateSignFlowRequest) []signTaskActor {
|
||
partyAActorID := c.config.ResolvePartyAActorID()
|
||
partyBActorID := c.config.ResolvePartyBActorID()
|
||
|
||
// 甲方先签 OrderNo=1:用户预览后「立即签署」进 iframe,完成短信验证 / 实名确认
|
||
partyA := signTaskActor{
|
||
Actor: &actor{
|
||
ActorType: "corp",
|
||
ActorID: partyAActorID,
|
||
ActorName: firstNonEmpty(strings.TrimSpace(req.PartyAName), partyAActorID),
|
||
Permissions: []string{"sign"},
|
||
ActorOpenID: strings.TrimSpace(req.PartyAOpenCorpID),
|
||
IdentNameForMatch: strings.TrimSpace(req.PartyAName),
|
||
CertNoForMatch: strings.TrimSpace(req.PartyAUSCC),
|
||
AccountName: strings.TrimSpace(req.TransactorMobile),
|
||
ClientUserID: firstNonEmpty(strings.TrimSpace(req.TransactorID), strings.TrimSpace(req.TransactorMobile)),
|
||
},
|
||
SignConfigInfo: &signConfigInfo{
|
||
OrderNo: 1,
|
||
JoinByLink: true,
|
||
RequestVerifyFree: false,
|
||
FreeLogin: false,
|
||
BlockHere: false,
|
||
},
|
||
}
|
||
|
||
// 乙方(平台)后签 OrderNo=2:甲方签完后由系统按模板免验证签自动盖章
|
||
partyB := signTaskActor{
|
||
Actor: &actor{
|
||
ActorType: "corp",
|
||
ActorID: partyBActorID,
|
||
ActorName: partyBActorID,
|
||
Permissions: []string{"sign"},
|
||
ActorOpenID: c.config.OpenCorpID,
|
||
},
|
||
SignConfigInfo: &signConfigInfo{
|
||
OrderNo: 2,
|
||
JoinByLink: true, // 链接加入;false 时要求 actorCorpMembers 非空
|
||
RequestVerifyFree: true,
|
||
FreeSignType: freeSignTypeTemplate,
|
||
FreeLogin: true,
|
||
BlockHere: false,
|
||
},
|
||
}
|
||
|
||
// 数组顺序与 orderNo 无强绑定(法大大按 orderNo 决定先后),保持 [甲方, 乙方] 便于阅读
|
||
return []signTaskActor{partyA, partyB}
|
||
}
|
||
|
||
func (c *Client) buildExpiresTime() string {
|
||
days := 7
|
||
if c.config.Contract != nil && c.config.Contract.ExpireDays > 0 {
|
||
days = c.config.Contract.ExpireDays
|
||
}
|
||
return strconv.FormatInt(time.Now().AddDate(0, 0, days).UnixMilli(), 10)
|
||
}
|
||
|
||
func validateCreateSignFlowRequest(req *CreateSignFlowRequest) error {
|
||
if req == nil {
|
||
return fmt.Errorf("创建签署请求不能为空")
|
||
}
|
||
if strings.TrimSpace(req.PartyAName) == "" && (req.Fill == nil || strings.TrimSpace(req.Fill.CompanyName) == "") {
|
||
return fmt.Errorf("甲方企业名不能为空")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, v := range values {
|
||
if strings.TrimSpace(v) != "" {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|