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 { RequestID string `json:"request_id"` Status string `json:"status"` Result json.RawMessage `json:"result"` Reason string `json:"reason"` } 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("id_number", 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() if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusForbidden { return ProviderResult{}, ErrProviderRateLimited } 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 { 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) } 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 } 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{ ProviderOrderNo: payload.RequestID, MaskedName: maskName(req.Name), EncryptedName: encryptedName, MaskedIDNo: maskIDNo(req.IDNo), EncryptedIDNo: encryptedIDNo, } switch result { case 1: base.Status = StatusVerified base.VerifiedAt = &now case 2: base.Status = StatusRejected base.FailReason = "姓名与身份证号不一致" case 3: base.Status = StatusRejected base.FailReason = "公安库无匹配记录" case 110: return ProviderResult{}, ErrProviderRateLimited default: base.Status = StatusRejected base.FailReason = fmt.Sprintf("未知核验结果:%d", result) } return base, nil } func cloudMarketHTTPError(statusCode int, reason string) error { reason = strings.TrimSpace(reason) switch statusCode { case http.StatusBadRequest: return fmt.Errorf("%w: %s", ErrInvalidRealnameInput, firstNonEmpty(reason, "cloud market bad request")) case http.StatusUnauthorized, http.StatusNotFound: return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderConfigInvalid, statusCode, reason) default: return fmt.Errorf("%w: cloud market http status %d: %s", ErrProviderUnavailable, statusCode, reason) } } func parseCloudMarketResult(raw json.RawMessage) (int, error) { if len(raw) == 0 || string(raw) == "null" { return 0, fmt.Errorf("%w: realname cloud market result missing", ErrProviderUnavailable) } var number int if err := json.Unmarshal(raw, &number); err == nil { return number, nil } 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 } } return 0, fmt.Errorf("%w: realname cloud market result invalid: %s", ErrProviderUnavailable, string(raw)) } func firstNonEmpty(values ...string) string { for _, value := range values { value = strings.TrimSpace(value) if value != "" { return value } } return "" }