Files
hyapi-server/internal/shared/fadada/signtask_service.go
2026-07-21 15:53:29 +08:00

418 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ""
}