258 lines
7.7 KiB
Go
258 lines
7.7 KiB
Go
package fadada
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"strings"
|
||
)
|
||
|
||
// 默认授权范围:认证信息 + 后续签署所需能力
|
||
var defaultAuthScopes = []string{
|
||
"ident_info",
|
||
"seal_info",
|
||
"signtask_info",
|
||
"signtask_init",
|
||
"signtask_file",
|
||
"organization",
|
||
"template",
|
||
}
|
||
|
||
// GenerateEnterpriseAuth 获取企业认证/授权链接(POST /corp/get-auth-url)
|
||
// 文档见 1.md:corpIdentType 置空由用户在页面选择组织类型。
|
||
func (c *Client) GenerateEnterpriseAuth(req *EnterpriseAuthRequest) (*EnterpriseAuthResult, error) {
|
||
if err := validateEnterpriseAuthRequest(req); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
clientCorpID := strings.TrimSpace(req.ClientCorpID)
|
||
if clientCorpID == "" {
|
||
clientCorpID = strings.TrimSpace(req.UnifiedSocialCode)
|
||
}
|
||
clientUserID := strings.TrimSpace(req.ClientUserID)
|
||
if clientUserID == "" {
|
||
clientUserID = strings.TrimSpace(req.TransactorID)
|
||
}
|
||
|
||
redirectURL := ""
|
||
if c.config.Auth != nil {
|
||
redirectURL = c.config.Auth.RedirectURL
|
||
}
|
||
|
||
apiReq := &getCorpAuthURLReq{
|
||
ClientCorpID: clientCorpID,
|
||
ClientUserID: clientUserID,
|
||
AccountName: strings.TrimSpace(req.TransactorMobile),
|
||
CorpIdentInfo: &corpIdentInfo{
|
||
CorpName: strings.TrimSpace(req.CompanyName),
|
||
// 按官方文档:不传默认为企业;置空让用户在页面选择组织类型
|
||
// CorpIdentType: "",
|
||
CorpIdentNo: strings.TrimSpace(req.UnifiedSocialCode),
|
||
LegalRepName: strings.TrimSpace(req.LegalPersonName),
|
||
CorpIdentMethod: []string{CorpIdentMethodLegalRep},
|
||
},
|
||
// 文档合法值仅 corpName/corpIdentType/corpIdentNo;不传表示都可修改
|
||
// CorpNonEditableInfo: ,
|
||
OprIdentInfo: &oprIdentInfo{
|
||
UserName: strings.TrimSpace(req.TransactorName),
|
||
UserIdentType: UserIdentTypeIDCard,
|
||
UserIdentNo: strings.TrimSpace(req.TransactorID),
|
||
Mobile: strings.TrimSpace(req.TransactorMobile),
|
||
OprIdentMethod: []string{OprIdentMethodMobile},
|
||
},
|
||
OprNonEditableInfo: nil,
|
||
AuthScopes: append([]string{}, defaultAuthScopes...),
|
||
RedirectURL: encodeRedirectURL(redirectURL),
|
||
}
|
||
|
||
res, err := c.http.PostBiz(pathGetCorpAuthURL, accessToken, apiReq)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, NewAPIError("获取法大大企业认证链接失败", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
|
||
var data getCorpAuthURLData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析企业认证链接响应失败: %w", err)
|
||
}
|
||
if data.AuthURL == "" {
|
||
return nil, fmt.Errorf("获取法大大企业认证链接失败: authUrl 为空 requestId=%s", res.RequestID)
|
||
}
|
||
|
||
return &EnterpriseAuthResult{
|
||
AuthFlowID: clientCorpID,
|
||
AuthURL: data.AuthURL,
|
||
AuthShortURL: data.AuthURL,
|
||
}, nil
|
||
}
|
||
|
||
// QueryCorpAuth 查询企业授权状态(POST /corp/get)
|
||
// 文档:https://dev.fadada.com/api-doc/X2JXRJIYRE/ELN3ZLQWR6PLXGCE/5-1
|
||
// 以 bindingStatus=authorized 判断是否已完成帐号授权绑定(比 get-auth-url 的 210002/210013 更准确)。
|
||
func (c *Client) QueryCorpAuth(req *QueryCorpAuthRequest) (*QueryCorpAuthResult, error) {
|
||
if req == nil {
|
||
return nil, fmt.Errorf("查询请求不能为空")
|
||
}
|
||
corpIdentNo := strings.TrimSpace(req.CorpIdentNo)
|
||
clientCorpID := strings.TrimSpace(req.ClientCorpID)
|
||
openCorpID := strings.TrimSpace(req.OpenCorpID)
|
||
filled := 0
|
||
if corpIdentNo != "" {
|
||
filled++
|
||
}
|
||
if clientCorpID != "" {
|
||
filled++
|
||
}
|
||
if openCorpID != "" {
|
||
filled++
|
||
}
|
||
if filled == 0 {
|
||
return nil, fmt.Errorf("corpIdentNo、clientCorpId、openCorpId 不能同时为空")
|
||
}
|
||
if filled > 1 {
|
||
// 文档要求三选一;业务优先级:openCorpId > corpIdentNo > clientCorpId
|
||
if openCorpID != "" {
|
||
corpIdentNo, clientCorpID = "", ""
|
||
} else if corpIdentNo != "" {
|
||
clientCorpID = ""
|
||
}
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
res, err := c.http.PostBiz(pathGetCorp, accessToken, &getCorpReq{
|
||
CorpIdentNo: corpIdentNo,
|
||
ClientCorpID: clientCorpID,
|
||
OpenCorpID: openCorpID,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
// 企业尚未在本应用下建档/授权时,视为未找到,不算业务异常
|
||
if isCorpNotFoundCode(res.Code) {
|
||
return &QueryCorpAuthResult{Found: false}, nil
|
||
}
|
||
return nil, NewAPIError("查询法大大企业授权状态失败", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
|
||
var data getCorpData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析企业授权状态失败: %w", err)
|
||
}
|
||
|
||
binding := strings.TrimSpace(data.BindingStatus)
|
||
ident := strings.TrimSpace(data.IdentStatus)
|
||
return &QueryCorpAuthResult{
|
||
Found: true,
|
||
Authorized: binding == BindingStatusAuthorized,
|
||
Identified: ident == IdentStatusIdentified,
|
||
ClientCorpID: strings.TrimSpace(data.ClientCorpID),
|
||
OpenCorpID: strings.TrimSpace(data.OpenCorpID),
|
||
BindingStatus: binding,
|
||
IdentStatus: ident,
|
||
AvailableStatus: strings.TrimSpace(data.AvailableStatus),
|
||
AuthScopes: append([]string{}, data.AuthScope...),
|
||
}, nil
|
||
}
|
||
|
||
func isCorpNotFoundCode(code string) bool {
|
||
return strings.TrimSpace(code) == CodeCorpNotFound
|
||
}
|
||
|
||
// QueryOrgVerified 查询企业是否已在法大大完成实名(POST /corp/get-identified-status)
|
||
func (c *Client) QueryOrgVerified(req *QueryOrgIdentityRequest) (bool, error) {
|
||
result, err := c.QueryOrgIdentity(req)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return result.Identified, nil
|
||
}
|
||
|
||
// QueryOrgIdentity 查询企业实名认证状态详情
|
||
func (c *Client) QueryOrgIdentity(req *QueryOrgIdentityRequest) (*QueryOrgIdentityResult, error) {
|
||
if req == nil {
|
||
return nil, fmt.Errorf("查询请求不能为空")
|
||
}
|
||
corpName := strings.TrimSpace(req.CorpName)
|
||
corpIdentNo := strings.TrimSpace(req.CorpIdentNo)
|
||
if corpName == "" && corpIdentNo == "" {
|
||
return nil, fmt.Errorf("企业名称与统一社会信用代码不能同时为空")
|
||
}
|
||
|
||
accessToken, err := c.ensureAccessToken()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
res, err := c.http.PostBiz(pathGetCorpIdentifiedStatus, accessToken, &getIdentifiedStatusReq{
|
||
CorpName: corpName,
|
||
CorpIdentNo: corpIdentNo,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if res.Code != successCode {
|
||
return nil, NewAPIError("查询法大大企业实名状态失败", res.Code, res.Msg, res.RequestID)
|
||
}
|
||
|
||
var data getIdentifiedStatusData
|
||
if err := decodeData(res.Data, &data); err != nil {
|
||
return nil, fmt.Errorf("解析企业实名状态失败: %w", err)
|
||
}
|
||
|
||
return &QueryOrgIdentityResult{
|
||
Identified: data.IdentStatus,
|
||
CorpName: corpName,
|
||
CorpIdentNo: corpIdentNo,
|
||
}, nil
|
||
}
|
||
|
||
func validateEnterpriseAuthRequest(req *EnterpriseAuthRequest) error {
|
||
if req == nil {
|
||
return fmt.Errorf("认证请求不能为空")
|
||
}
|
||
if strings.TrimSpace(req.CompanyName) == "" {
|
||
return fmt.Errorf("企业名称不能为空")
|
||
}
|
||
if strings.TrimSpace(req.UnifiedSocialCode) == "" {
|
||
return fmt.Errorf("统一社会信用代码不能为空")
|
||
}
|
||
if strings.TrimSpace(req.LegalPersonName) == "" {
|
||
return fmt.Errorf("法人姓名不能为空")
|
||
}
|
||
if strings.TrimSpace(req.TransactorName) == "" {
|
||
return fmt.Errorf("经办人姓名不能为空")
|
||
}
|
||
if strings.TrimSpace(req.TransactorMobile) == "" {
|
||
return fmt.Errorf("经办人手机号不能为空")
|
||
}
|
||
if strings.TrimSpace(req.TransactorID) == "" {
|
||
return fmt.Errorf("经办人身份证号不能为空")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// encodeRedirectURL 按官方文档对 redirectUrl 做 URL encode;已编码则原样返回
|
||
func encodeRedirectURL(raw string) string {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
if strings.Contains(raw, "%") {
|
||
if decoded, err := url.QueryUnescape(raw); err == nil && decoded != raw {
|
||
return raw
|
||
}
|
||
}
|
||
return url.QueryEscape(raw)
|
||
}
|