package fadada import ( "fmt" "strconv" "strings" "time" ) // CreateSignFlow 基于签署任务模板创建签署任务(POST /sign-task/create-with-template) // - autoStart:无填单时创建即自动提交;有填单时创建后填控件再 Start(完成自动提交) // - 签署顺序:甲方 orderNo=1 先签(短信/验证);乙方 orderNo=2 后签,由系统按模板免验证签自动盖章 // - 乙方参与方开启免验证签(requestVerifyFree=true + freeSignType=template);requestVerifyFree 时 businessId 必传 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 businessID, err := c.resolveBusinessID() if err != nil { return nil, err } 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: businessID, // requestVerifyFree=true 时必传场景码 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" // 根据模板免验证签;requestVerifyFree 时仍须传 businessId // resolveBusinessID 免验证签场景码(requestVerifyFree=true 时 businessId 必传) func (c *Client) resolveBusinessID() (string, error) { if c.config == nil { return "", fmt.Errorf("法大大配置为空") } businessID := strings.TrimSpace(c.config.NoAuthSceneCode) if businessID == "" { return "", fmt.Errorf("法大大 no_auth_scene_code(免验证签场景码)不能为空:requestVerifyFree 时 businessId 必传") } return businessID, nil } 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:甲方签完后由系统按模板免验证签自动盖章(任务级传 businessId) 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 "" }