Files

199 lines
5.4 KiB
Go

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 ""
}