Files
hyapi-server/internal/shared/fadada/signtask_service.go

418 lines
13 KiB
Go
Raw Normal View History

2026-07-21 15:53:29 +08:00
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 ""
}