feat(realname): 实名核验切换到数脉二要素新接口
服务商旧接口 sinocheck2 已下线,改走 eid.shumaidata.com/eid/check: - 身份证入参 id_number 改为 idcard - 响应改为先判业务 code、再读 result.res 的两级结构, code/res 兼容字符串与数字两种返回形式 - 按服务商错误码表分流:400/20010/20310 归入参错误, 404/604 归服务未配置,其余归服务不可用 - 驳回原因优先取服务商 description - 新接口不返回流水号,provider_order_no 对该渠道留空 - 同步更新默认 URL 与 docs/实名认证.md Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,10 +29,35 @@ type CloudMarketProvider struct {
|
||||
}
|
||||
|
||||
type cloudMarketResponse struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Status string `json:"status"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Reason string `json:"reason"`
|
||||
Code flexString `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
type cloudMarketCheckResult struct {
|
||||
Res flexString `json:"res"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// flexString 兼容服务商把 code、res 返回成字符串或数字两种形式。
|
||||
type flexString string
|
||||
|
||||
func (f *flexString) UnmarshalJSON(data []byte) error {
|
||||
trimmed := strings.TrimSpace(string(data))
|
||||
if trimmed == "" || trimmed == "null" {
|
||||
*f = ""
|
||||
return nil
|
||||
}
|
||||
if trimmed[0] == '"' {
|
||||
var value string
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*f = flexString(strings.TrimSpace(value))
|
||||
return nil
|
||||
}
|
||||
*f = flexString(trimmed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCloudMarketProvider(cfg CloudMarketConfig) (*CloudMarketProvider, error) {
|
||||
@@ -67,7 +92,7 @@ func (p *CloudMarketProvider) Start(ctx context.Context, req StartRequest) (Prov
|
||||
}
|
||||
query := endpoint.Query()
|
||||
query.Set("name", strings.TrimSpace(req.Name))
|
||||
query.Set("id_number", strings.TrimSpace(req.IDNo))
|
||||
query.Set("idcard", strings.TrimSpace(req.IDNo))
|
||||
endpoint.RawQuery = query.Encode()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil)
|
||||
@@ -94,19 +119,17 @@ func (p *CloudMarketProvider) Start(ctx context.Context, req StartRequest) (Prov
|
||||
|
||||
var payload cloudMarketResponse
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return ProviderResult{}, cloudMarketHTTPError(resp.StatusCode, "")
|
||||
}
|
||||
return ProviderResult{}, fmt.Errorf("%w: invalid cloud market response: %v", ErrProviderUnavailable, err)
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return ProviderResult{}, cloudMarketHTTPError(resp.StatusCode, payload.Reason)
|
||||
return ProviderResult{}, cloudMarketHTTPError(resp.StatusCode, payload.Message)
|
||||
}
|
||||
if strings.ToUpper(strings.TrimSpace(payload.Status)) != "OK" {
|
||||
return ProviderResult{
|
||||
ProviderOrderNo: payload.RequestID,
|
||||
Status: StatusRejected,
|
||||
MaskedName: maskName(req.Name),
|
||||
MaskedIDNo: maskIDNo(req.IDNo),
|
||||
FailReason: firstNonEmpty(payload.Reason, "实名认证请求失败"),
|
||||
}, nil
|
||||
// 先判断 code,code 非 0 表示调用失败,无需再看 result。
|
||||
if code := string(payload.Code); code != "0" {
|
||||
return ProviderResult{}, cloudMarketCodeError(code, payload.Message)
|
||||
}
|
||||
|
||||
result, err := parseCloudMarketResult(payload.Result)
|
||||
@@ -126,65 +149,65 @@ func (p *CloudMarketProvider) Start(ctx context.Context, req StartRequest) (Prov
|
||||
|
||||
now := time.Now()
|
||||
base := ProviderResult{
|
||||
ProviderOrderNo: payload.RequestID,
|
||||
MaskedName: maskName(req.Name),
|
||||
EncryptedName: encryptedName,
|
||||
MaskedIDNo: maskIDNo(req.IDNo),
|
||||
EncryptedIDNo: encryptedIDNo,
|
||||
MaskedName: maskName(req.Name),
|
||||
EncryptedName: encryptedName,
|
||||
MaskedIDNo: maskIDNo(req.IDNo),
|
||||
EncryptedIDNo: encryptedIDNo,
|
||||
}
|
||||
switch result {
|
||||
case 1:
|
||||
switch string(result.Res) {
|
||||
case "1":
|
||||
base.Status = StatusVerified
|
||||
base.VerifiedAt = &now
|
||||
case 2:
|
||||
case "2":
|
||||
base.Status = StatusRejected
|
||||
base.FailReason = "姓名与身份证号不一致"
|
||||
case 3:
|
||||
base.FailReason = firstNonEmpty(result.Description, "姓名与身份证号不一致")
|
||||
case "3":
|
||||
base.Status = StatusRejected
|
||||
base.FailReason = "公安库无匹配记录"
|
||||
case 110:
|
||||
return ProviderResult{}, ErrProviderRateLimited
|
||||
base.FailReason = firstNonEmpty(result.Description, "公安库无匹配记录")
|
||||
default:
|
||||
base.Status = StatusRejected
|
||||
base.FailReason = fmt.Sprintf("未知核验结果:%d", result)
|
||||
base.FailReason = firstNonEmpty(result.Description, fmt.Sprintf("未知核验结果:%s", result.Res))
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func cloudMarketHTTPError(statusCode int, reason string) error {
|
||||
reason = strings.TrimSpace(reason)
|
||||
func cloudMarketHTTPError(statusCode int, message string) error {
|
||||
message = strings.TrimSpace(message)
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return fmt.Errorf("%w: %s", ErrInvalidRealnameInput, firstNonEmpty(reason, "cloud market bad request"))
|
||||
return fmt.Errorf("%w: %s", ErrInvalidRealnameInput, firstNonEmpty(message, "cloud market bad request"))
|
||||
case http.StatusUnauthorized, http.StatusNotFound:
|
||||
return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderConfigInvalid, statusCode, reason)
|
||||
return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderConfigInvalid, statusCode, message)
|
||||
default:
|
||||
return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderUnavailable, statusCode, reason)
|
||||
return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderUnavailable, statusCode, message)
|
||||
}
|
||||
}
|
||||
|
||||
func parseCloudMarketResult(raw json.RawMessage) (int, error) {
|
||||
// cloudMarketCodeError 按服务商错误码表映射,code 取值见 docs/实名认证.md。
|
||||
func cloudMarketCodeError(code, message string) error {
|
||||
message = strings.TrimSpace(message)
|
||||
switch code {
|
||||
case "400", "20010", "20310":
|
||||
return fmt.Errorf("%w: cloud market code %s: %s", ErrInvalidRealnameInput, code, message)
|
||||
case "404", "604":
|
||||
return fmt.Errorf("%w: cloud market code %s: %s", ErrProviderConfigInvalid, code, message)
|
||||
default:
|
||||
return fmt.Errorf("%w: cloud market code %s: %s", ErrProviderUnavailable, firstNonEmpty(code, "empty"), message)
|
||||
}
|
||||
}
|
||||
|
||||
func parseCloudMarketResult(raw json.RawMessage) (cloudMarketCheckResult, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return 0, fmt.Errorf("%w: realname cloud market result missing", ErrProviderUnavailable)
|
||||
return cloudMarketCheckResult{}, fmt.Errorf("%w: realname cloud market result missing", ErrProviderUnavailable)
|
||||
}
|
||||
var number int
|
||||
if err := json.Unmarshal(raw, &number); err == nil {
|
||||
return number, nil
|
||||
var result cloudMarketCheckResult
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
return cloudMarketCheckResult{}, fmt.Errorf("%w: realname cloud market result invalid: %s", ErrProviderUnavailable, string(raw))
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err == nil {
|
||||
switch strings.TrimSpace(text) {
|
||||
case "1":
|
||||
return 1, nil
|
||||
case "2":
|
||||
return 2, nil
|
||||
case "3":
|
||||
return 3, nil
|
||||
case "110":
|
||||
return 110, nil
|
||||
}
|
||||
if strings.TrimSpace(string(result.Res)) == "" {
|
||||
return cloudMarketCheckResult{}, fmt.Errorf("%w: realname cloud market res missing: %s", ErrProviderUnavailable, string(raw))
|
||||
}
|
||||
return 0, fmt.Errorf("%w: realname cloud market result invalid: %s", ErrProviderUnavailable, string(raw))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
|
||||
Reference in New Issue
Block a user