This commit is contained in:
Mrx
2026-05-29 17:51:30 +08:00
parent a5a0522c91
commit 2a174e49e5
12 changed files with 295 additions and 1819 deletions

View File

@@ -1019,6 +1019,10 @@ type QYGL5F6AReq struct {
IDCard string `json:"id_card" validate:"required,validIDCard"` IDCard string `json:"id_card" validate:"required,validIDCard"`
} }
type QYGLVR76Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type IVYZ6G7HReq struct { type IVYZ6G7HReq struct {
Name string `json:"name" validate:"required,min=1,validName"` Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"` IDCard string `json:"id_card" validate:"required,validIDCard"`

View File

@@ -384,6 +384,7 @@ func registerAllProcessors(combService *comb.CombService) {
"QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443 "QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443
"QCXG3M7Z": qcxg.ProcessQCXG3M7ZRequest, //人车关系核验ETC10093 月更 "QCXG3M7Z": qcxg.ProcessQCXG3M7ZRequest, //人车关系核验ETC10093 月更
"QCXGM4CL": qcxg.ProcessQCXGM4CLRequest, //名下车辆诺尔 "QCXGM4CL": qcxg.ProcessQCXGM4CLRequest, //名下车辆诺尔
"QYGLVR76": qygl.ProcessQYGLVR76Request, //名下企业诺尔
// DWBG系列处理器 - 多维报告 // DWBG系列处理器 - 多维报告
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest, "DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
"DWBG8B4D": dwbg.ProcessDWBG8B4DRequest, "DWBG8B4D": dwbg.ProcessDWBG8B4DRequest,

View File

@@ -288,6 +288,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
"QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai "QYGL2YSB": &dto.QYGL2YSBReq{}, //企业二要素认证shumai
"QYGLDG77": &dto.QYGLDG77Req{}, //企业对公打款认证shumai "QYGLDG77": &dto.QYGLDG77Req{}, //企业对公打款认证shumai
"QCXGM4CL": &dto.QCXGM4CLReq{}, //名下车辆诺尔 "QCXGM4CL": &dto.QCXGM4CLReq{}, //名下车辆诺尔
"QYGLVR76": &dto.QYGLVR76Req{}, //名下企业诺尔
} }
// 优先返回已配置的DTO // 优先返回已配置的DTO

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +0,0 @@
## 响应示例
### 成功响应
```json
{
"currently_overdue": "2",
"max_overdue_amt": "1-1000",
"acc_sleep": "14",
"currently_performance": "12",
"result_code": "1",
"max_overdue_days": "16-30",
"latest_overdue_time": "2023-05",
"acc_exc": "0"
}
```

View File

@@ -1,19 +0,0 @@
# result 参数说明
| 参数名 | 类型 | 说明 |
| ---- | ---- | ---- |
| score | Number | 分数区间 0-100分数越高表明风险越大 |
| riskCode | String | 风险码可选值21001、21002、11001、11002、11003、11004、11005、12001、12002<br>风险等级1 低风险、2 中风险、3 高风险 |
# riskCode风险标签编码说明
| 编码 | 标签名称 | 描述说明 |
| ---- | ---- | ---- |
| 21001 | 疑似恶意借贷 | 恶意多方借贷、以贷养贷、蓄意制造借贷纠纷等违规行为 |
| 21002 | 疑似职业撸口子 | 职业从事网络贷款撸口子,无还款意愿的恶意行为 |
| 11001 | 疑似涉黑涉赌 | 涉嫌参与传销、在线赌博等违法行为 |
| 11002 | 疑似网络投机 | 参与在线外汇、虚拟币、石油贵金属等风险投机行为 |
| 11003 | 疑似营销欺诈 | 在网络平台运营活动中,组团薅羊毛、套利等违规行为 |
| 11004 | 疑似黑中介包装 | 中介包装伪造冒用资料、黑产中介圈团成员、老赖反催收等 |
| 11005 | 疑似恶意套现 | 信用卡恶意套现、第三方平台消费分期套现等违规行为 |
| 12001 | 疑似黑产设备 | 存在模拟器、多开、群控、代理等作弊行为的黑产设备 |
| 12002 | 疑似黑产账号 | 疑似黑产组织非法包装的手机号、身份证、支付等账号 |

View File

@@ -1,14 +0,0 @@
{
"result": {
"acc_exc": "0",
"acc_sleep": "14",
"result_code": "1",
"max_overdue_amt": "1-1000",
"max_overdue_days": "16-30",
"currently_overdue": "2",
"latest_overdue_time": "2023-05",
"currently_performance": "12"
},
"busiMsg": "success",
"busiCode": 10
}

View File

@@ -4,13 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"math"
"strconv"
"strings"
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/zhicha" "tyapi-server/internal/infrastructure/external/nuoer"
) )
// ProcessJRZQ8B3CRequest JRZQ8B3C API处理方法 - 个人消费能力等级 // ProcessJRZQ8B3CRequest JRZQ8B3C API处理方法 - 个人消费能力等级
@@ -24,173 +21,37 @@ func ProcessJRZQ8B3CRequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) body := map[string]string{
if err != nil { "name": paramsDto.Name,
return nil, errors.Join(processors.ErrSystem, err) "idCard": paramsDto.IDCard,
"mobile": paramsDto.MobileNo,
} }
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) nuoerDoCheckAPIKey := "consumerTagV1"
if err != nil { ApiPath := "/v1/doCheck"
return nil, errors.Join(processors.ErrSystem, err)
}
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo) resp, err := deps.NuoerService.CallAPI(ctx, nuoerDoCheckAPIKey, ApiPath, body)
if err != nil { if err != nil {
return nil, errors.Join(processors.ErrSystem, err) if errors.Is(err, nuoer.ErrDatasource) {
}
reqData := map[string]interface{}{
"name": encryptedName,
"idCard": encryptedIDCard,
"phone": encryptedMobileNo,
"authorized": "1",
}
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI034", reqData)
if err != nil {
if errors.Is(err, zhicha.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
} }
if errors.Is(err, nuoer.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
} }
personIncomeIndex := "-1" rawData, ok := resp.Data.(map[string]interface{})
if m, ok := respData.(map[string]interface{}); ok { if !ok {
personIncomeIndex = mapTap010ToIncomeIndex(m["tap010"], paramsDto.IDCard) return nil, errors.Join(processors.ErrSystem, errors.New("响应格式错误"))
} }
respPayload := map[string]interface{}{ result := mapNuoerConsumerTagToResponse(rawData)
"personincome_index_2.0": personIncomeIndex,
}
respBytes, err := json.Marshal(respPayload) respBytes, err := json.Marshal(result)
if err != nil { if err != nil {
return nil, errors.Join(processors.ErrSystem, err) return nil, errors.Join(processors.ErrSystem, err)
} }
return respBytes, nil return respBytes, nil
} }
type incomeTier struct {
Score int
Low float64
High float64 // 上界闭区间math.Inf(1) 表示正无穷
}
var incomeTiers = []incomeTier{
{Score: 100, Low: 1000, High: 2000},
{Score: 200, Low: 2000, High: 4000},
{Score: 300, Low: 4000, High: 6000},
{Score: 400, Low: 6000, High: 8000},
{Score: 500, Low: 8000, High: 10000},
{Score: 600, Low: 10000, High: 12000},
{Score: 700, Low: 12000, High: 15000},
{Score: 800, Low: 15000, High: 20000},
{Score: 900, Low: 20000, High: 25000},
{Score: 1000, Low: 25000, High: math.Inf(1)},
}
func mapTap010ToIncomeIndex(rawTap010 interface{}, idCard string) string {
tap010, ok := parseTap010Level(rawTap010)
if !ok {
return "-1"
}
mappedLow, mappedHigh := expandTap010Range(tap010)
candidateScores := intersectedTierScores(mappedLow, mappedHigh)
if len(candidateScores) == 0 {
return "-1"
}
seed := stableSeedFromIDCard(idCard)
score := candidateScores[seed%len(candidateScores)]
return strconv.Itoa(score)
}
func parseTap010Level(v interface{}) (int, bool) {
switch value := v.(type) {
case string:
value = strings.TrimSpace(value)
if value == "" {
return 0, false
}
n, err := strconv.Atoi(value)
if err != nil {
return 0, false
}
if n < 1 || n > 4 {
return 0, false
}
return n, true
case float64:
n := int(value)
if value != float64(n) || n < 1 || n > 4 {
return 0, false
}
return n, true
default:
return 0, false
}
}
func expandTap010Range(level int) (float64, float64) {
// tap010 原区间:
// 1:(0,500) 2:[500,1000) 3:[1000,3000) 4:[3000,+inf)
// 按比例放大 9 倍映射到收入尺度,满足示例: (0,500)->(0,4500)
switch level {
case 1:
return 0, 4500
case 2:
return 4500, 9000
case 3:
return 9000, 27000
case 4:
return 27000, math.Inf(1)
default:
return 0, 0
}
}
func intersectedTierScores(low, high float64) []int {
scores := make([]int, 0, len(incomeTiers))
for _, t := range incomeTiers {
if isRangeIntersect(low, high, t.Low, t.High) {
scores = append(scores, t.Score)
}
}
return scores
}
func isRangeIntersect(aLow, aHigh, bLow, bHigh float64) bool {
return aLow <= bHigh && bLow <= aHigh
}
func stableSeedFromIDCard(idCard string) int {
if len(idCard) == 0 {
return 0
}
runes := []rune(idCard)
start := len(runes) - 4
if start < 0 {
start = 0
}
seed := 0
for _, r := range runes[start:] {
switch {
case r >= '0' && r <= '9':
seed = seed*11 + int(r-'0')
case r == 'X' || r == 'x':
seed = seed*11 + 10
default:
seed = seed*11 + int(r)%11
}
}
if seed < 0 {
return -seed
}
return seed
}

View File

@@ -0,0 +1,104 @@
package jrzq
import (
"strconv"
)
type consumerTagScoreInterval struct {
min float64
max float64
minInclusive bool
maxInclusive bool
output string
}
// nuoer score 取值范围 350-850按 50 分一档映射为 10 档 personincome_index_2.0100-1000
var consumerTagScoreIntervals = []consumerTagScoreInterval{
{min: 350, max: 400, minInclusive: true, maxInclusive: false, output: "100"},
{min: 400, max: 450, minInclusive: true, maxInclusive: false, output: "200"},
{min: 450, max: 500, minInclusive: true, maxInclusive: false, output: "300"},
{min: 500, max: 550, minInclusive: true, maxInclusive: false, output: "400"},
{min: 550, max: 600, minInclusive: true, maxInclusive: false, output: "500"},
{min: 600, max: 650, minInclusive: true, maxInclusive: false, output: "600"},
{min: 650, max: 700, minInclusive: true, maxInclusive: false, output: "700"},
{min: 700, max: 750, minInclusive: true, maxInclusive: false, output: "800"},
{min: 750, max: 800, minInclusive: true, maxInclusive: false, output: "900"},
{min: 800, max: 850, minInclusive: true, maxInclusive: true, output: "1000"},
}
// mapNuoerConsumerTagToResponse 将 nuoer data2json.md转为 JRZQ8B3C 对外结构1json.md
func mapNuoerConsumerTagToResponse(data map[string]interface{}) map[string]interface{} {
score, ok := extractConsumerTagScore(data)
return map[string]interface{}{
"personincome_index_2.0": mapConsumerTagScoreToIncomeIndex(score, ok),
}
}
func extractConsumerTagScore(data map[string]interface{}) (float64, bool) {
if data == nil {
return 0, false
}
payload := data
if result, ok := data["result"].(map[string]interface{}); ok {
payload = result
}
return parseConsumerTagScore(payload["score"])
}
func parseConsumerTagScore(v interface{}) (float64, bool) {
if v == nil {
return 0, false
}
switch val := v.(type) {
case float64:
return val, true
case int:
return float64(val), true
case int64:
return float64(val), true
case string:
if val == "" {
return 0, false
}
f, err := strconv.ParseFloat(val, 64)
if err != nil {
return 0, false
}
return f, true
default:
return 0, false
}
}
func mapConsumerTagScoreToIncomeIndex(score float64, ok bool) string {
if !ok {
return "-1"
}
if score < 350 {
return "-1"
}
if score > 850 {
return "1000"
}
for _, rule := range consumerTagScoreIntervals {
if consumerTagScoreInInterval(score, rule) {
return rule.output
}
}
return "-1"
}
func consumerTagScoreInInterval(score float64, rule consumerTagScoreInterval) bool {
if rule.minInclusive {
if score < rule.min {
return false
}
} else if score <= rule.min {
return false
}
if rule.maxInclusive {
return score <= rule.max
}
return score < rule.max
}

View File

@@ -4,11 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"tyapi-server/internal/domains/api/dto" "tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors" "tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/xingwei" "tyapi-server/internal/infrastructure/external/nuoer"
) )
// ProcessQYGL5F6ARequest QYGL5F6A API处理方法 - 企业相关查询 // ProcessQYGL5F6ARequest QYGL5F6A API处理方法 - 企业相关查询
@@ -21,27 +20,34 @@ func ProcessQYGL5F6ARequest(ctx context.Context, params []byte, deps *processors
if err := deps.Validator.ValidateStruct(paramsDto); err != nil { if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err) return nil, errors.Join(processors.ErrInvalidParam, err)
} }
fmt.Println("paramsDto", paramsDto) body := map[string]string{
"idCard": paramsDto.IDCard,
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
reqData := map[string]interface{}{
"idCardNum": paramsDto.IDCard,
} }
// 调用行为数据API使用指定的project_id nuoerDoCheckAPIKey := "idRelationV101"
projectID := "CDJ-1101695397213958144" ApiPath := "/v1/doCheck"
fmt.Println("reqData", reqData)
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData) resp, err := deps.NuoerService.CallAPI(ctx, nuoerDoCheckAPIKey, ApiPath, body)
if err != nil { if err != nil {
if errors.Is(err, xingwei.ErrNotFound) { if errors.Is(err, nuoer.ErrDatasource) {
return nil, errors.Join(processors.ErrNotFound, err)
} else if errors.Is(err, xingwei.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err) return nil, errors.Join(processors.ErrDatasource, err)
} else if errors.Is(err, xingwei.ErrSystem) {
return nil, errors.Join(processors.ErrSystem, err)
} else {
return nil, errors.Join(processors.ErrSystem, err)
} }
if errors.Is(err, nuoer.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
rawData, ok := resp.Data.(map[string]interface{})
if !ok {
return nil, errors.Join(processors.ErrSystem, errors.New("响应格式错误"))
}
result := mapNuoerIdRelationToEntReportResponse(rawData)
respBytes, err := json.Marshal(result)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
} }
return respBytes, nil return respBytes, nil

View File

@@ -0,0 +1,95 @@
package qygl
var qygl5f6aBasicInfoFields = []string{
"regStatus", "estiblishTime", "regCapital", "industry", "staffList", "type",
"regCapitalCurrency", "legalPersonName", "regNumber", "creditCode", "name",
"companyOrgType", "base",
}
// mapNuoerIdRelationToEntReportResponse 将 nuoer data2json.md转为 QYGL5F6A 对外结构1json.md
// 解包 result映射 datalist 项,并包装为 ent_report_001.queryResult.items。
func mapNuoerIdRelationToEntReportResponse(data map[string]interface{}) map[string]interface{} {
items := make([]interface{}, 0)
if data != nil {
payload := unwrapNuoerIdRelationData(data)
rawList := asSlice(payload["datalist"])
items = make([]interface{}, 0, len(rawList))
for _, item := range rawList {
if mapped := mapQygl5f6aItem(asMap(item)); mapped != nil {
items = append(items, mapped)
}
}
}
return map[string]interface{}{
"ent_report_001": map[string]interface{}{
"queryResult": map[string]interface{}{
"items": items,
},
},
}
}
func mapQygl5f6aItem(item map[string]interface{}) map[string]interface{} {
if len(item) == 0 {
return nil
}
out := make(map[string]interface{}, 8)
for _, key := range []string{"orgName", "pName", "relationship"} {
if val, ok := item[key]; ok && val != nil {
out[key] = val
}
}
if basicInfo := mapQygl5f6aBasicInfo(asMap(item["basicInfo"])); isNonemptyMap(basicInfo) {
out["basicInfo"] = basicInfo
}
stockHolder := pickFields(asMap(item["stockHolderItem"]), qygl6f2dStockHolderFields)
if !isNonemptyMap(stockHolder) {
stockHolder = pickFields(asMap(item["his_stockHolderItem"]), qygl6f2dStockHolderFields)
}
if isNonemptyMap(stockHolder) {
out["stockHolderItem"] = stockHolder
}
if adminPenalty := mapNuoerIdRelationRecords(item["adminPenalty"], qygl6f2dAdminPenaltyFields); len(adminPenalty) > 0 {
out["adminPenalty"] = adminPenalty
}
if executedPerson := mapNuoerIdRelationRecords(item["executedPerson"], qygl6f2dExecutedPersonFields); len(executedPerson) > 0 {
out["executedPerson"] = executedPerson
}
if dishonestExecutedPerson := mapNuoerIdRelationRecords(item["dishonestExecutedPerson"], qygl6f2dDishonestExecutedPersonFields); len(dishonestExecutedPerson) > 0 {
out["dishonestExecutedPerson"] = dishonestExecutedPerson
}
return out
}
func mapQygl5f6aBasicInfo(src map[string]interface{}) map[string]interface{} {
if len(src) == 0 {
return nil
}
basicInfo := pickFields(src, qygl5f6aBasicInfoFields)
if staffList, ok := basicInfo["staffList"]; !ok || isEmptyValue(staffList) {
if hisStaffList := src["his_staffList"]; !isEmptyValue(hisStaffList) {
basicInfo["staffList"] = hisStaffList
} else {
delete(basicInfo, "staffList")
}
}
return basicInfo
}
func isEmptyValue(v interface{}) bool {
if v == nil {
return true
}
if m, ok := v.(map[string]interface{}); ok {
return len(m) == 0
}
if s, ok := v.([]interface{}); ok {
return len(s) == 0
}
return false
}

View File

@@ -0,0 +1,48 @@
package qygl
import (
"context"
"encoding/json"
"errors"
"tyapi-server/internal/domains/api/dto"
"tyapi-server/internal/domains/api/services/processors"
"tyapi-server/internal/infrastructure/external/nuoer"
)
// ProcessQYGLVR76Request QYGLVR76 API处理方法 -人企关联诺尔
func ProcessQYGLVR76Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QYGLVR76Req
if err := json.Unmarshal(params, &paramsDto); err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
return nil, errors.Join(processors.ErrInvalidParam, err)
}
body := map[string]string{
"idCard": paramsDto.IDCard,
}
nuoerDoCheckAPIKey := "idRelationV101"
ApiPath := "/v1/doCheck"
resp, err := deps.NuoerService.CallAPI(ctx, nuoerDoCheckAPIKey, ApiPath, body)
if err != nil {
if errors.Is(err, nuoer.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, nuoer.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}