This commit is contained in:
2026-07-21 15:53:29 +08:00
parent 024b6614ba
commit c943f575d5
40 changed files with 5362 additions and 275 deletions

View File

@@ -0,0 +1,204 @@
package esign
import (
"context"
"encoding/json"
"fmt"
"strings"
"hyapi-server/internal/domains/certification/enums"
"hyapi-server/internal/domains/certification/ports"
sharedesign "hyapi-server/internal/shared/esign"
)
type Provider struct {
client *sharedesign.Client
}
func NewProvider(client *sharedesign.Client) *Provider {
return &Provider{client: client}
}
func (p *Provider) Platform() enums.SignPlatform {
return enums.SignPlatformEsign
}
func (p *Provider) GenerateEnterpriseAuth(ctx context.Context, req *ports.EnterpriseAuthRequest) (*ports.AuthLinkResult, error) {
_ = ctx
res, err := p.client.GenerateEnterpriseAuth(&sharedesign.EnterpriseAuthRequest{
CompanyName: req.CompanyName,
UnifiedSocialCode: req.UnifiedSocialCode,
LegalPersonName: req.LegalPersonName,
LegalPersonID: req.LegalPersonID,
TransactorName: req.TransactorName,
TransactorMobile: req.TransactorMobile,
TransactorID: req.TransactorID,
})
if err != nil {
return nil, err
}
return &ports.AuthLinkResult{
AuthFlowID: res.AuthFlowID,
AuthURL: res.AuthURL,
AuthShortURL: firstNonEmpty(res.AuthShortURL, res.AuthURL),
}, nil
}
func (p *Provider) QueryOrgVerified(ctx context.Context, req *ports.OrgIdentityQuery) (bool, error) {
_ = ctx
identity, err := p.client.QueryOrgIdentityInfo(&sharedesign.QueryOrgIdentityRequest{
OrgName: req.OrgName,
OrgIDCardNum: req.OrgIdentNo,
})
if err != nil {
return false, err
}
return identity != nil && identity.Data.RealnameStatus == 1, nil
}
func (p *Provider) GenerateContractFile(ctx context.Context, req *ports.ContractGenerateRequest) (*ports.ContractFileResult, error) {
_ = ctx
components := map[string]string{
"jfqym": req.CompanyName,
"jfqym2": req.CompanyName,
"jfsqdb": req.AuthorizedRepName,
"jftyshxydm": req.UnifiedSocialCode,
"jflxdz": req.EnterpriseAddress,
"xybh": req.AgreementNo,
"qsrq1": req.SignDate,
"qsrq3": req.SignDate,
"qsrq2": req.SignDate,
}
res, err := p.client.FillTemplate(components)
if err != nil {
return nil, err
}
return &ports.ContractFileResult{
FileID: res.FileID,
FileDownloadURL: res.FileDownloadUrl,
}, nil
}
func (p *Provider) CreateSignFlow(ctx context.Context, req *ports.SignFlowCreateRequest) (*ports.SignFlowResult, error) {
_ = ctx
flowID, err := p.client.CreateSignFlow(&sharedesign.CreateSignFlowRequest{
FileID: req.FileID,
SignerAccount: req.PartyAUSCC,
SignerName: req.PartyAName,
TransactorPhone: req.TransactorMobile,
TransactorName: req.TransactorName,
TransactorIDCardNum: req.TransactorID,
})
if err != nil {
return nil, err
}
if req.SkipActorURL {
return &ports.SignFlowResult{SignFlowID: flowID}, nil
}
longURL, shortURL, err := p.client.GetSignURL(flowID, req.TransactorMobile, req.PartyAName)
if err != nil {
return nil, err
}
return &ports.SignFlowResult{
SignFlowID: flowID,
SignURL: firstNonEmpty(longURL, shortURL),
ShortURL: shortURL,
}, nil
}
func (p *Provider) GetActorSignURL(ctx context.Context, req *ports.GetActorSignURLRequest) (*ports.ActorSignURLResult, error) {
_ = ctx
if req == nil || strings.TrimSpace(req.SignFlowID) == "" {
return nil, fmt.Errorf("signFlowId 不能为空")
}
longURL, shortURL, err := p.client.GetSignURL(req.SignFlowID, req.TransactorMobile, req.PartyAName)
if err != nil {
return nil, err
}
return &ports.ActorSignURLResult{
SignFlowID: req.SignFlowID,
EmbedURL: firstNonEmpty(longURL, shortURL),
ShortURL: shortURL,
}, nil
}
func (p *Provider) GetSignTaskPreviewURL(ctx context.Context, signFlowID string) (*ports.SignTaskPreviewURLResult, error) {
_ = ctx
// e签宝无独立预览接口用已签/合同文件下载链作为预览源(前端用 PDF 打开)
files, err := p.DownloadSignedFiles(ctx, signFlowID)
if err != nil {
return nil, err
}
if len(files) == 0 || strings.TrimSpace(files[0].DownloadURL) == "" {
return nil, fmt.Errorf("未获取到可预览的合同文件")
}
return &ports.SignTaskPreviewURLResult{
SignFlowID: signFlowID,
PreviewURL: files[0].DownloadURL,
}, nil
}
func (p *Provider) QuerySignStatus(ctx context.Context, flowID string) (*ports.SignStatusResult, error) {
_ = ctx
detail, err := p.client.QuerySignFlowDetail(flowID)
if err != nil {
return nil, err
}
status := detail.Data.SignFlowStatus
return &ports.SignStatusResult{
SignFlowID: flowID,
Status: fmt.Sprintf("%d", status),
Completed: status == 2,
Rejected: status == 7,
Expired: status == 5,
Message: detail.Data.SignFlowDescription,
}, nil
}
func (p *Provider) DownloadSignedFiles(ctx context.Context, flowID string) ([]*ports.SignedFile, error) {
_ = ctx
res, err := p.client.DownloadSignedFile(flowID)
if err != nil {
return nil, err
}
files := make([]*ports.SignedFile, 0, len(res.Data.Files))
for _, f := range res.Data.Files {
files = append(files, &ports.SignedFile{DownloadURL: f.DownloadUrl})
}
return files, nil
}
func (p *Provider) VerifyCallback(ctx context.Context, headers map[string]string, body []byte) error {
_ = ctx
_ = headers
_ = body
// e签宝现有回调走独立验签逻辑此处保持兼容由原 HandleEsignCallback 处理)
return nil
}
func (p *Provider) ParseCallback(ctx context.Context, headers map[string]string, body []byte) (*ports.CallbackEvent, error) {
_ = ctx
_ = headers
var raw map[string]interface{}
_ = json.Unmarshal(body, &raw)
action, _ := raw["action"].(string)
ev := &ports.CallbackEvent{Event: action, Raw: raw}
if action == "AUTH_PASS" {
ev.AuthPassed = true
}
if action == "SIGN_FLOW_COMPLETE" || action == "SIGN_MISSON_COMPLETE" {
ev.SignCompleted = true
}
return ev, nil
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
var _ ports.SignPlatformProvider = (*Provider)(nil)

View File

@@ -0,0 +1,247 @@
package fadada
import (
"context"
"fmt"
"net/url"
"strings"
"hyapi-server/internal/domains/certification/enums"
"hyapi-server/internal/domains/certification/ports"
sharedfadada "hyapi-server/internal/shared/fadada"
)
type Provider struct {
client *sharedfadada.Client
}
func NewProvider(client *sharedfadada.Client) *Provider {
return &Provider{client: client}
}
func (p *Provider) Platform() enums.SignPlatform {
return enums.SignPlatformFadada
}
func (p *Provider) GenerateEnterpriseAuth(ctx context.Context, req *ports.EnterpriseAuthRequest) (*ports.AuthLinkResult, error) {
_ = ctx
res, err := p.client.GenerateEnterpriseAuth(&sharedfadada.EnterpriseAuthRequest{
ClientCorpID: req.ClientCorpID,
ClientUserID: req.ClientUserID,
CompanyName: req.CompanyName,
UnifiedSocialCode: req.UnifiedSocialCode,
LegalPersonName: req.LegalPersonName,
LegalPersonID: req.LegalPersonID,
TransactorName: req.TransactorName,
TransactorMobile: req.TransactorMobile,
TransactorID: req.TransactorID,
})
if err != nil {
return nil, err
}
return &ports.AuthLinkResult{
AuthFlowID: res.AuthFlowID,
AuthURL: res.AuthURL,
AuthShortURL: firstNonEmpty(res.AuthShortURL, res.AuthURL),
}, nil
}
func (p *Provider) QueryOrgVerified(ctx context.Context, req *ports.OrgIdentityQuery) (bool, error) {
_ = ctx
return p.client.QueryOrgVerified(&sharedfadada.QueryOrgIdentityRequest{
CorpName: req.OrgName,
CorpIdentNo: req.OrgIdentNo,
})
}
// GenerateContractFile 法大大走签署任务模板(/sign-task/create-with-template
// 此处返回空结果,实际填单由 CreateSignFlow 通过 /sign-task/field/fill-values 完成。
func (p *Provider) GenerateContractFile(ctx context.Context, req *ports.ContractGenerateRequest) (*ports.ContractFileResult, error) {
_ = ctx
_ = req
return &ports.ContractFileResult{FileID: "", FileDownloadURL: ""}, nil
}
func (p *Provider) CreateSignFlow(ctx context.Context, req *ports.SignFlowCreateRequest) (*ports.SignFlowResult, error) {
_ = ctx
createReq := &sharedfadada.CreateSignFlowRequest{
Subject: req.Subject,
PartyAOpenCorpID: req.PartyAOpenCorpID,
PartyAName: req.PartyAName,
PartyAUSCC: req.PartyAUSCC,
TransactorName: req.TransactorName,
TransactorMobile: req.TransactorMobile,
TransactorID: req.TransactorID,
TransReferenceID: req.TransReferenceID,
}
if req.Fill != nil {
createReq.Fill = &sharedfadada.ContractFillRequest{
AgreementNo: req.Fill.AgreementNo,
CompanyName: req.Fill.CompanyName,
UnifiedSocialCode: req.Fill.UnifiedSocialCode,
EnterpriseAddress: req.Fill.EnterpriseAddress,
AuthorizedRepName: req.Fill.AuthorizedRepName,
SignDate: req.Fill.SignDate,
FileName: req.Fill.FileName,
}
} else {
// 签署模板创建必须填控件
createReq.Fill = &sharedfadada.ContractFillRequest{
CompanyName: req.PartyAName,
UnifiedSocialCode: req.PartyAUSCC,
AuthorizedRepName: req.TransactorName,
}
}
createRes, err := p.client.CreateSignFlow(createReq)
if err != nil {
return nil, err
}
if req.SkipActorURL {
return &ports.SignFlowResult{
SignFlowID: createRes.SignTaskID,
}, nil
}
urlRes, err := p.client.GetSignURL(createRes.SignTaskID, sharedfadada.ActorIDPartyA)
if err != nil {
return nil, err
}
embedURL := strings.TrimSpace(urlRes.EmbedURL)
if embedURL == "" {
return nil, fmt.Errorf("法大大未返回可嵌入签署链接 actorSignTaskEmbedUrl")
}
return &ports.SignFlowResult{
SignFlowID: createRes.SignTaskID,
SignURL: embedURL,
ShortURL: strings.TrimSpace(urlRes.SignURL),
}, nil
}
func (p *Provider) GetActorSignURL(ctx context.Context, req *ports.GetActorSignURLRequest) (*ports.ActorSignURLResult, error) {
_ = ctx
if req == nil || strings.TrimSpace(req.SignFlowID) == "" {
return nil, fmt.Errorf("signFlowId 不能为空")
}
urlRes, err := p.client.GetSignURL(strings.TrimSpace(req.SignFlowID), sharedfadada.ActorIDPartyA)
if err != nil {
return nil, err
}
embedURL := strings.TrimSpace(urlRes.EmbedURL)
if embedURL == "" {
return nil, fmt.Errorf("法大大未返回可嵌入签署链接 actorSignTaskEmbedUrl")
}
return &ports.ActorSignURLResult{
SignFlowID: req.SignFlowID,
EmbedURL: embedURL,
ShortURL: strings.TrimSpace(urlRes.SignURL),
}, nil
}
func (p *Provider) GetSignTaskPreviewURL(ctx context.Context, signFlowID string) (*ports.SignTaskPreviewURLResult, error) {
_ = ctx
signFlowID = strings.TrimSpace(signFlowID)
if signFlowID == "" {
return nil, fmt.Errorf("signFlowId 不能为空")
}
res, err := p.client.GetSignTaskPreviewURL(signFlowID)
if err != nil {
return nil, err
}
return &ports.SignTaskPreviewURLResult{
SignFlowID: signFlowID,
PreviewURL: res.PreviewURL,
}, nil
}
func (p *Provider) QuerySignStatus(ctx context.Context, flowID string) (*ports.SignStatusResult, error) {
_ = ctx
st, err := p.client.QuerySignStatus(flowID)
if err != nil {
return nil, err
}
return &ports.SignStatusResult{
SignFlowID: flowID,
Status: st.SignTaskStatus,
Completed: st.Completed,
Rejected: st.Terminated,
Expired: false,
Message: firstNonEmpty(st.TerminationNote, st.RevokeReason),
}, nil
}
func (p *Provider) DownloadSignedFiles(ctx context.Context, flowID string) ([]*ports.SignedFile, error) {
_ = ctx
res, err := p.client.DownloadSignedFiles(flowID)
if err != nil {
return nil, err
}
files := make([]*ports.SignedFile, 0, len(res.Files))
for _, f := range res.Files {
files = append(files, &ports.SignedFile{
DownloadURL: f.DownloadURL,
DownloadID: f.DownloadID,
})
}
return files, nil
}
func (p *Provider) VerifyCallback(ctx context.Context, headers map[string]string, body []byte) error {
_ = ctx
bizContent := extractBizContent(body)
return p.client.VerifyCallback(headers, bizContent)
}
func (p *Provider) ParseCallback(ctx context.Context, headers map[string]string, body []byte) (*ports.CallbackEvent, error) {
_ = ctx
bizContent := extractBizContent(body)
ev, err := p.client.ParseCallback(headers, bizContent)
if err != nil {
return nil, err
}
out := &ports.CallbackEvent{
Event: ev.Event,
SignFlowID: ev.SignTaskID,
AuthFlowID: firstNonEmpty(ev.ClientCorpID, ev.OpenCorpID),
Raw: ev.Raw,
}
if ev.IsSignCompletedCallback() {
out.SignCompleted = true
}
// 企业授权/认证成功事件
switch strings.ToLower(ev.Event) {
case "corp-authorize", "corp-authorize-success", "corporation_authorize", "auth-pass":
out.AuthPassed = true
}
if strings.EqualFold(ev.AuthResult, "success") || strings.EqualFold(ev.AuthResult, "pass") {
out.AuthPassed = true
}
return out, nil
}
func extractBizContent(body []byte) string {
raw := strings.TrimSpace(string(body))
if raw == "" {
return ""
}
if strings.HasPrefix(raw, "{") {
return raw
}
values, err := url.ParseQuery(raw)
if err == nil {
if v := values.Get("bizContent"); v != "" {
return v
}
}
return raw
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
var _ ports.SignPlatformProvider = (*Provider)(nil)

View File

@@ -0,0 +1,52 @@
package signplatform
import (
"fmt"
"hyapi-server/internal/domains/certification/enums"
"hyapi-server/internal/domains/certification/ports"
)
type Registry struct {
providers map[enums.SignPlatform]ports.SignPlatformProvider
order []enums.SignPlatform
}
func NewRegistry(providers ...ports.SignPlatformProvider) *Registry {
r := &Registry{
providers: make(map[enums.SignPlatform]ports.SignPlatformProvider, len(providers)),
order: make([]enums.SignPlatform, 0, len(providers)),
}
for _, p := range providers {
if p == nil {
continue
}
platform := p.Platform()
r.providers[platform] = p
r.order = append(r.order, platform)
}
return r
}
func (r *Registry) Get(platform enums.SignPlatform) (ports.SignPlatformProvider, error) {
if platform == "" {
platform = enums.DefaultSignPlatform()
}
p, ok := r.providers[platform]
if !ok {
return nil, fmt.Errorf("不支持的签署平台: %s", platform)
}
return p, nil
}
func (r *Registry) List() []ports.SignPlatformProvider {
out := make([]ports.SignPlatformProvider, 0, len(r.order))
for _, platform := range r.order {
if p, ok := r.providers[platform]; ok {
out = append(out, p)
}
}
return out
}
var _ ports.SignPlatformRegistry = (*Registry)(nil)

View File

@@ -305,11 +305,10 @@ func (s *QiNiuStorageService) DownloadFile(ctx context.Context, fileURL string)
isTimeout = true
} else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
isTimeout = true
} else if errStr := err.Error();
errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
} else if errStr := err.Error(); errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
isTimeout = true
}

View File

@@ -154,6 +154,59 @@ func (h *CertificationHandler) ConfirmAuth(c *gin.Context) {
h.response.Success(c, result, "状态确认成功")
}
// ListSignPlatforms 可选签署平台列表
func (h *CertificationHandler) ListSignPlatforms(c *gin.Context) {
result, err := h.appService.ListSignPlatforms(c.Request.Context())
if err != nil {
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, result, "获取签署平台列表成功")
}
// SelectSignPlatform 选择签署平台并生成企业认证链接
func (h *CertificationHandler) SelectSignPlatform(c *gin.Context) {
var cmd commands.SelectSignPlatformCommand
cmd.UserID = h.getCurrentUserID(c)
if cmd.UserID == "" {
h.response.Unauthorized(c, "用户未登录")
return
}
if err := c.ShouldBindJSON(&cmd); err != nil {
h.response.BadRequest(c, "请求参数错误")
return
}
result, err := h.appService.SelectSignPlatform(c.Request.Context(), &cmd)
if err != nil {
h.logger.Error("选择签署平台失败", zap.Error(err), zap.String("user_id", cmd.UserID))
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, result, "已选择签署平台")
}
// HandleFadadaCallback 法大大回调
func (h *CertificationHandler) HandleFadadaCallback(c *gin.Context) {
headers := make(map[string]string)
for key, values := range c.Request.Header {
if len(values) > 0 {
headers[key] = values[0]
}
}
body, err := io.ReadAll(c.Request.Body)
if err != nil {
h.logger.Error("读取法大大回调失败", zap.Error(err))
c.String(400, "fail")
return
}
if err := h.appService.HandleFadadaCallback(c.Request.Context(), headers, body); err != nil {
h.logger.Error("处理法大大回调失败", zap.Error(err))
c.String(400, "fail")
return
}
c.Data(200, "application/json", []byte(`{"msg":"success"}`))
}
// ConfirmSign 前端确认是否完成签署
// @Summary 前端确认签署状态
// @Description 前端轮询确认合同签署是否完成
@@ -219,6 +272,61 @@ func (h *CertificationHandler) ApplyContract(c *gin.Context) {
h.response.Success(c, result, "合同申请成功")
}
// RefreshContractSignURL 刷新合同签署 iframe 长链
// @Summary 刷新合同签署链接
// @Description 重新获取可供 iframe 嵌入的签署长链(法大大约 10 分钟/单次有效)
// @Tags 认证管理
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} responses.ContractSignUrlResponse "刷新成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 401 {object} map[string]interface{} "未认证"
// @Router /api/v1/certifications/refresh-contract-sign-url [post]
func (h *CertificationHandler) RefreshContractSignURL(c *gin.Context) {
userID := h.getCurrentUserID(c)
if userID == "" {
h.response.Unauthorized(c, "用户未登录")
return
}
result, err := h.appService.RefreshContractSignURL(c.Request.Context(), userID)
if err != nil {
h.logger.Error("刷新签署链接失败", zap.Error(err), zap.String("user_id", userID))
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, result, "签署链接已刷新")
}
// RefreshContractPreviewURL 刷新合同预览链接
// @Summary 刷新合同预览链接
// @Description 获取签署任务预览链接(法大大 /sign-task/get-preview-url
// @Tags 认证管理
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} responses.ContractPreviewUrlResponse "刷新成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Router /api/v1/certifications/refresh-contract-preview-url [post]
func (h *CertificationHandler) RefreshContractPreviewURL(c *gin.Context) {
userID := h.getCurrentUserID(c)
if userID == "" {
h.response.Unauthorized(c, "用户未登录")
return
}
result, err := h.appService.RefreshContractPreviewURL(c.Request.Context(), userID)
if err != nil {
h.logger.Error("刷新合同预览链接失败", zap.Error(err), zap.String("user_id", userID))
h.response.BadRequest(c, err.Error())
return
}
h.response.Success(c, result, "预览链接已刷新")
}
// RecognizeBusinessLicense OCR识别营业执照
// @Summary OCR识别营业执照
// @Description 上传营业执照图片进行OCR识别自动填充企业信息

View File

@@ -65,10 +65,17 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
// 3. 申请合同签署
authGroup.POST("/apply-contract", r.handler.ApplyContract)
// 刷新签署 iframe 长链(法大大 EmbedURL 短期有效)
authGroup.POST("/refresh-contract-sign-url", r.handler.RefreshContractSignURL)
authGroup.POST("/refresh-contract-preview-url", r.handler.RefreshContractPreviewURL)
// 前端确认是否完成认证
authGroup.POST("/confirm-auth", r.handler.ConfirmAuth)
// 签署平台选择(审核通过后、企业认证前)
authGroup.GET("/sign-platforms", r.handler.ListSignPlatforms)
authGroup.POST("/select-sign-platform", r.handler.SelectSignPlatform)
// 前端确认是否完成签署
authGroup.POST("/confirm-sign", r.handler.ConfirmSign)
@@ -95,7 +102,8 @@ func (r *CertificationRoutes) Register(router *http.GinRouter) {
// 回调路由(不需要认证,但需要验证签名)
callbackGroup := certificationGroup.Group("/callbacks")
{
callbackGroup.POST("/esign", r.handler.HandleEsignCallback) // e签宝回调(统一处理企业认证和合同签署回调)
callbackGroup.POST("/esign", r.handler.HandleEsignCallback) // e签宝回调
callbackGroup.POST("/fadada", r.handler.HandleFadadaCallback) // 法大大回调
}
}