package realname import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "hfb_sys/backend/pkg/crypto" ) const cloudMarketProviderName = "aliyun_cloudmarket" type CloudMarketConfig struct { URL string AppCode string Encryptor crypto.Encryptor } type CloudMarketProvider struct { url string appCode string client *http.Client encryptor crypto.Encryptor } type cloudMarketResponse struct { 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) { endpoint := strings.TrimSpace(cfg.URL) appCode := strings.TrimSpace(cfg.AppCode) if endpoint == "" || appCode == "" { return nil, ErrProviderConfigInvalid } if _, err := url.ParseRequestURI(endpoint); err != nil { return nil, fmt.Errorf("realname cloud market url invalid: %w", err) } encryptor := cfg.Encryptor if encryptor == nil { encryptor = &crypto.MockEncryptor{} } return &CloudMarketProvider{ url: endpoint, appCode: appCode, client: &http.Client{Timeout: 10 * time.Second}, encryptor: encryptor, }, nil } func (p *CloudMarketProvider) Name() string { return cloudMarketProviderName } func (p *CloudMarketProvider) Start(ctx context.Context, req StartRequest) (ProviderResult, error) { endpoint, err := url.Parse(p.url) if err != nil { return ProviderResult{}, err } query := endpoint.Query() query.Set("name", strings.TrimSpace(req.Name)) query.Set("idcard", strings.TrimSpace(req.IDNo)) endpoint.RawQuery = query.Encode() httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), nil) if err != nil { return ProviderResult{}, err } httpReq.Header.Set("Authorization", "APPCODE "+p.appCode) httpReq.Header.Set("Accept", "application/json") resp, err := p.client.Do(httpReq) if err != nil { return ProviderResult{}, fmt.Errorf("%w: realname cloud market request failed: %v", ErrProviderUnavailable, err) } defer resp.Body.Close() // 网关层错误不会带业务 code,原因只在这两个响应头里。 gatewayCode := strings.TrimSpace(resp.Header.Get("X-Ca-Error-Code")) gatewayMessage := strings.TrimSpace(resp.Header.Get("X-Ca-Error-Message")) if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusForbidden { if isQuotaExhausted(gatewayCode, gatewayMessage) { return ProviderResult{}, fmt.Errorf("%w: cloud market %s: %s", ErrProviderQuotaExhausted, gatewayCode, gatewayMessage) } return ProviderResult{}, fmt.Errorf("%w: cloud market %s: %s", ErrProviderRateLimited, gatewayCode, gatewayMessage) } body, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) if err != nil { return ProviderResult{}, fmt.Errorf("%w: read cloud market response: %v", ErrProviderUnavailable, err) } var payload cloudMarketResponse if err := json.Unmarshal(body, &payload); err != nil { payload = cloudMarketResponse{} } // 服务商失败时 HTTP 状态码也会跟着变(如身份证号错误返回 400), // 但业务 code 才是权威结果,只有拿不到 code 时才退回按状态码判断。 code := strings.TrimSpace(string(payload.Code)) if code == "" { return ProviderResult{}, cloudMarketHTTPError(resp.StatusCode, firstNonEmpty(gatewayMessage, truncateForLog(body))) } if code != "0" { return ProviderResult{}, cloudMarketCodeError(code, payload.Message) } result, err := parseCloudMarketResult(payload.Result) if err != nil { return ProviderResult{}, err } // 加密完整信息 encryptedName, err := p.encryptor.Encrypt(req.Name) if err != nil { return ProviderResult{}, err } encryptedIDNo, err := p.encryptor.Encrypt(req.IDNo) if err != nil { return ProviderResult{}, err } now := time.Now() base := ProviderResult{ MaskedName: maskName(req.Name), EncryptedName: encryptedName, MaskedIDNo: maskIDNo(req.IDNo), EncryptedIDNo: encryptedIDNo, } switch string(result.Res) { case "1": base.Status = StatusVerified base.VerifiedAt = &now case "2": base.Status = StatusRejected base.FailReason = firstNonEmpty(result.Description, "姓名与身份证号不一致") case "3": base.Status = StatusRejected base.FailReason = firstNonEmpty(result.Description, "公安库无匹配记录") default: base.Status = StatusRejected base.FailReason = firstNonEmpty(result.Description, fmt.Sprintf("未知核验结果:%s", result.Res)) } return base, nil } // isQuotaExhausted 区分额度耗尽与调用频率过高:两者都是 HTTP 403,但额度耗尽靠重试不会恢复。 func isQuotaExhausted(gatewayCode, gatewayMessage string) bool { if strings.EqualFold(gatewayCode, "B403MQ") { return true } return strings.Contains(strings.ToLower(gatewayMessage), "quota exhausted") } func cloudMarketHTTPError(statusCode int, detail string) error { detail = strings.TrimSpace(detail) switch { case statusCode == http.StatusBadRequest: return fmt.Errorf("%w: %s", ErrInvalidRealnameInput, firstNonEmpty(detail, "cloud market bad request")) case statusCode == http.StatusUnauthorized, statusCode == http.StatusNotFound: return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderConfigInvalid, statusCode, detail) case statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices: return fmt.Errorf("%w: cloud market response without code: %s", ErrProviderUnavailable, detail) default: return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderUnavailable, statusCode, detail) } } func truncateForLog(body []byte) string { const limit = 200 text := strings.TrimSpace(string(body)) runes := []rune(text) if len(runes) <= limit { return text } return string(runes[:limit]) + "..." } // 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 cloudMarketCheckResult{}, fmt.Errorf("%w: realname cloud market result missing", ErrProviderUnavailable) } 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)) } if strings.TrimSpace(string(result.Res)) == "" { return cloudMarketCheckResult{}, fmt.Errorf("%w: realname cloud market res missing: %s", ErrProviderUnavailable, string(raw)) } return result, nil } func firstNonEmpty(values ...string) string { for _, value := range values { value = strings.TrimSpace(value) if value != "" { return value } } return "" }