新增v2特征,评分接口;数据过滤

This commit is contained in:
wangxiu 2026-07-17 13:51:19 +08:00
parent 9e0729f6f5
commit 849ebe8f4d
5 changed files with 491 additions and 65 deletions

View File

@ -1,5 +1,6 @@
package com.lyxh.api.docking.controller.ftapi;
import com.lyxh.api.docking.enums.ResultFilterType;
import com.lyxh.api.docking.model.ftapi.PaymentProfileRequest;
import com.lyxh.api.docking.model.ftapi.PaymentProfileResponse;
import com.lyxh.api.docking.service.PaymentProfileService;
@ -20,14 +21,37 @@ public class FortispayController {
@Autowired
private PaymentProfileService paymentProfileService;
/** 印尼支付行为画像 - v1 */
/** 印尼支付行为画像 - V1 特征 */
@PostMapping("/payment-feature")
public ResponseEntity<PaymentProfileResponse> getPaymentFeature(
@Valid @RequestBody PaymentProfileRequest request,
@RequestHeader(value = "X-Request-Id", required = false) String xRequestId,
@RequestHeader(value = "X-Language", required = false) String language) {
return executeQuery(request, xRequestId, language, ResultFilterType.V1_FEATURE);
}
/** 印尼支付行为画像 - V2 特征(除综合评分外的全量 V2.2 字段) */
@PostMapping("/payment-feature-v2")
public ResponseEntity<PaymentProfileResponse> getPaymentFeatureV2(
@Valid @RequestBody PaymentProfileRequest request,
@RequestHeader(value = "X-Request-Id", required = false) String xRequestId,
@RequestHeader(value = "X-Language", required = false) String language) {
return executeQuery(request, xRequestId, language, ResultFilterType.V2_FEATURE);
}
/** 印尼支付行为画像 - V2 综合评分 */
@PostMapping("/payment-feature-v2-score")
public ResponseEntity<PaymentProfileResponse> getPaymentFeatureV2Score(
@Valid @RequestBody PaymentProfileRequest request,
@RequestHeader(value = "X-Request-Id", required = false) String xRequestId,
@RequestHeader(value = "X-Language", required = false) String language) {
return executeQuery(request, xRequestId, language, ResultFilterType.V2_SCORE);
}
private ResponseEntity<PaymentProfileResponse> executeQuery(
PaymentProfileRequest request, String xRequestId, String language, ResultFilterType filterType) {
String weOrderNo = UUID.randomUUID().toString().replace("-", "");
PaymentProfileResponse body = paymentProfileService.query(request, xRequestId, language, weOrderNo);
PaymentProfileResponse body = paymentProfileService.query(request, xRequestId, language, weOrderNo, filterType);
ResponseEntity.BodyBuilder builder = ResponseEntity.ok()
.header("X-Request-Id", xRequestId != null ? xRequestId : weOrderNo);
if (body.getResponseId() != null && !body.getResponseId().isEmpty()) {

View File

@ -0,0 +1,13 @@
package com.lyxh.api.docking.enums;
/**
* result 字段过滤模式
*/
public enum ResultFilterType {
/** 只保留 V1.xlsx 的字段(536 字段,6 模块) */
V1_FEATURE,
/** 保留 V2.2.xlsx 的特征字段(606 字段,6 模块,排除 overall_score) */
V2_FEATURE,
/** 只保留 V2.2.xlsx 的 overall_score 字段(15 字段,1 模块) */
V2_SCORE
}

View File

@ -4,10 +4,12 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lyxh.api.docking.enums.ApiType;
import com.lyxh.api.docking.enums.ResultFilterType;
import com.lyxh.api.docking.exception.ApiCallException;
import com.lyxh.api.docking.model.ftapi.FortispayRequest;
import com.lyxh.api.docking.model.ftapi.PaymentProfileRequest;
import com.lyxh.api.docking.model.ftapi.PaymentProfileResponse;
import com.lyxh.api.docking.util.ResultFieldFilter;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,7 +37,7 @@ public class PaymentProfileService {
private ObjectMapper objectMapper;
public PaymentProfileResponse query(PaymentProfileRequest request, String xRequestId, String language,
String weOrderNo) {
String weOrderNo, ResultFilterType filterType) {
boolean isEncrypted = request.getEncryptAlgo() != null && !request.getEncryptAlgo().isBlank();
String idCardForCache = isEncrypted ? request.getIdCard() : DigestUtils.md5Hex(request.getIdCard());
String mobileForCache = isEncrypted ? request.getMobile() : DigestUtils.md5Hex(request.getMobile());
@ -67,7 +69,7 @@ public class PaymentProfileService {
this::shouldCache
);
JsonNode root = objectMapper.readTree(respJson);
return parseResponse(root);
return parseResponse(root, filterType);
} catch (JsonProcessingException e) {
throw new ApiCallException("PARSE_ERROR", "Failed to parse Fortispay response: " + e.getMessage(), null);
} catch (ApiCallException e) {
@ -103,7 +105,7 @@ public class PaymentProfileService {
}
}
private PaymentProfileResponse parseResponse(JsonNode node) {
private PaymentProfileResponse parseResponse(JsonNode node, ResultFilterType filterType) {
PaymentProfileResponse response = new PaymentProfileResponse();
response.setCode(node.path("code").asText());
response.setMessage(node.path("message").asText());
@ -119,6 +121,7 @@ public class PaymentProfileService {
if (result != null) {
@SuppressWarnings("unchecked")
Map<String, Object> resultMap = objectMapper.convertValue(result, Map.class);
ResultFieldFilter.filter(resultMap, filterType);
dataNode.setResult(resultMap);
}
response.setData(dataNode);

View File

@ -0,0 +1,301 @@
package com.lyxh.api.docking.util;
import com.lyxh.api.docking.enums.ResultFilterType;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 支付行为画像 result 字段过滤器
* 根据 富通数据服务接口数据字典 提取 V1/V2.2 字段白名单
*/
public final class ResultFieldFilter {
private static final Map<String, Set<String>> V1_WHITELIST = Map.of(
"app_behavior", Set.of(
"apply_cnt_day3", "apply_cnt_day7", "apply_cnt_day14", "apply_cnt_day30",
"apply_cnt_day60", "apply_cnt_day90", "apply_cnt_day180", "apply_cnt_day240",
"apply_cnt_day365", "apply_cnt_day730",
"apply_org_cnt_day3", "apply_org_cnt_day7", "apply_org_cnt_day14", "apply_org_cnt_day30",
"apply_org_cnt_day60", "apply_org_cnt_day90", "apply_org_cnt_day180", "apply_org_cnt_day240",
"apply_org_cnt_day365", "apply_org_cnt_day730",
"apply_daily_max_cnt_day3", "apply_daily_max_cnt_day7", "apply_daily_max_cnt_day14", "apply_daily_max_cnt_day30",
"apply_daily_max_cnt_day60", "apply_daily_max_cnt_day90", "apply_daily_max_cnt_day180", "apply_daily_max_cnt_day240",
"apply_daily_max_cnt_day365", "apply_daily_max_cnt_day730",
"apply_daily_max_org_cnt_day3", "apply_daily_max_org_cnt_day7", "apply_daily_max_org_cnt_day14", "apply_daily_max_org_cnt_day30",
"apply_daily_max_org_cnt_day60", "apply_daily_max_org_cnt_day90", "apply_daily_max_org_cnt_day180", "apply_daily_max_org_cnt_day240",
"apply_daily_max_org_cnt_day365", "apply_daily_max_org_cnt_day730",
"apply_has_record_day_cnt_day3", "apply_has_record_day_cnt_day7", "apply_has_record_day_cnt_day14", "apply_has_record_day_cnt_day30",
"apply_has_record_day_cnt_day60", "apply_has_record_day_cnt_day90", "apply_has_record_day_cnt_day180", "apply_has_record_day_cnt_day240",
"apply_has_record_day_cnt_day365", "apply_has_record_day_cnt_day730",
"apply_max_org_cnt_rate_day3", "apply_max_org_cnt_rate_day7", "apply_max_org_cnt_rate_day14", "apply_max_org_cnt_rate_day30",
"apply_max_org_cnt_rate_day60", "apply_max_org_cnt_rate_day90", "apply_max_org_cnt_rate_day180", "apply_max_org_cnt_rate_day240",
"apply_max_org_cnt_rate_day365", "apply_max_org_cnt_rate_day730",
"apply_mean_delta_day3", "apply_mean_delta_day7", "apply_mean_delta_day14", "apply_mean_delta_day30",
"apply_mean_delta_day60", "apply_mean_delta_day90", "apply_mean_delta_day180", "apply_mean_delta_day240",
"apply_mean_delta_day365", "apply_mean_delta_day730",
"apply_max_delta_day3", "apply_max_delta_day7", "apply_max_delta_day14", "apply_max_delta_day30",
"apply_max_delta_day60", "apply_max_delta_day90", "apply_max_delta_day180", "apply_max_delta_day240",
"apply_max_delta_day365", "apply_max_delta_day730",
"apply_min_delta_day3", "apply_min_delta_day7", "apply_min_delta_day14", "apply_min_delta_day30",
"apply_min_delta_day60", "apply_min_delta_day90", "apply_min_delta_day180", "apply_min_delta_day240",
"apply_min_delta_day365", "apply_min_delta_day730"
),
"loan_behavior", Set.of(
"credit_cnt_day3", "credit_cnt_day7", "credit_cnt_day14", "credit_cnt_day30",
"credit_cnt_day60", "credit_cnt_day90", "credit_cnt_day180", "credit_cnt_day240",
"credit_cnt_day365", "credit_cnt_day730",
"credit_org_cnt_day3", "credit_org_cnt_day7", "credit_org_cnt_day14", "credit_org_cnt_day30",
"credit_org_cnt_day60", "credit_org_cnt_day90", "credit_org_cnt_day180", "credit_org_cnt_day240",
"credit_org_cnt_day365", "credit_org_cnt_day730",
"credit_daily_max_cnt_day3", "credit_daily_max_cnt_day7", "credit_daily_max_cnt_day14", "credit_daily_max_cnt_day30",
"credit_daily_max_cnt_day60", "credit_daily_max_cnt_day90", "credit_daily_max_cnt_day180", "credit_daily_max_cnt_day240",
"credit_daily_max_cnt_day365", "credit_daily_max_cnt_day730",
"credit_daily_max_org_cnt_day3", "credit_daily_max_org_cnt_day7", "credit_daily_max_org_cnt_day14", "credit_daily_max_org_cnt_day30",
"credit_daily_max_org_cnt_day60", "credit_daily_max_org_cnt_day90", "credit_daily_max_org_cnt_day180", "credit_daily_max_org_cnt_day240",
"credit_daily_max_org_cnt_day365", "credit_daily_max_org_cnt_day730",
"credit_has_record_day_cnt_day3", "credit_has_record_day_cnt_day7", "credit_has_record_day_cnt_day14", "credit_has_record_day_cnt_day30",
"credit_has_record_day_cnt_day60", "credit_has_record_day_cnt_day90", "credit_has_record_day_cnt_day180", "credit_has_record_day_cnt_day240",
"credit_has_record_day_cnt_day365", "credit_has_record_day_cnt_day730",
"credit_max_org_cnt_rate_day3", "credit_max_org_cnt_rate_day7", "credit_max_org_cnt_rate_day14", "credit_max_org_cnt_rate_day30",
"credit_max_org_cnt_rate_day60", "credit_max_org_cnt_rate_day90", "credit_max_org_cnt_rate_day180", "credit_max_org_cnt_rate_day240",
"credit_max_org_cnt_rate_day365", "credit_max_org_cnt_rate_day730",
"credit_mean_delta_day3", "credit_mean_delta_day7", "credit_mean_delta_day14", "credit_mean_delta_day30",
"credit_mean_delta_day60", "credit_mean_delta_day90", "credit_mean_delta_day180", "credit_mean_delta_day240",
"credit_mean_delta_day365", "credit_mean_delta_day730",
"credit_max_delta_day3", "credit_max_delta_day7", "credit_max_delta_day14", "credit_max_delta_day30",
"credit_max_delta_day60", "credit_max_delta_day90", "credit_max_delta_day180", "credit_max_delta_day240",
"credit_max_delta_day365", "credit_max_delta_day730",
"credit_min_delta_day3", "credit_min_delta_day7", "credit_min_delta_day14", "credit_min_delta_day30",
"credit_min_delta_day60", "credit_min_delta_day90", "credit_min_delta_day180", "credit_min_delta_day240",
"credit_min_delta_day365", "credit_min_delta_day730",
"credit_total_amt_day3", "credit_total_amt_day7", "credit_total_amt_day14", "credit_total_amt_day30",
"credit_total_amt_day60", "credit_total_amt_day90", "credit_total_amt_day180", "credit_total_amt_day240",
"credit_total_amt_day365", "credit_total_amt_day730",
"credit_avg_amt_day3", "credit_avg_amt_day7", "credit_avg_amt_day14", "credit_avg_amt_day30",
"credit_avg_amt_day60", "credit_avg_amt_day90", "credit_avg_amt_day180", "credit_avg_amt_day240",
"credit_avg_amt_day365", "credit_avg_amt_day730",
"credit_min_amt_day3", "credit_min_amt_day7", "credit_min_amt_day14", "credit_min_amt_day30",
"credit_min_amt_day60", "credit_min_amt_day90", "credit_min_amt_day180", "credit_min_amt_day240",
"credit_min_amt_day365", "credit_min_amt_day730",
"credit_max_amt_day3", "credit_max_amt_day7", "credit_max_amt_day14", "credit_max_amt_day30",
"credit_max_amt_day60", "credit_max_amt_day90", "credit_max_amt_day180", "credit_max_amt_day240",
"credit_max_amt_day365", "credit_max_amt_day730",
"credit_pass_rate_day3", "credit_pass_rate_day7", "credit_pass_rate_day14", "credit_pass_rate_day30",
"credit_pass_rate_day60", "credit_pass_rate_day90", "credit_pass_rate_day180", "credit_pass_rate_day240",
"credit_pass_rate_day365", "credit_pass_rate_day730"
),
"repay_behavior", Set.of(
"repay_cnt_day3", "repay_cnt_day7", "repay_cnt_day14", "repay_cnt_day30",
"repay_cnt_day60", "repay_cnt_day90", "repay_cnt_day180", "repay_cnt_day240",
"repay_cnt_day365", "repay_cnt_day730",
"repay_org_cnt_day3", "repay_org_cnt_day7", "repay_org_cnt_day14", "repay_org_cnt_day30",
"repay_org_cnt_day60", "repay_org_cnt_day90", "repay_org_cnt_day180", "repay_org_cnt_day240",
"repay_org_cnt_day365", "repay_org_cnt_day730",
"repay_daily_max_cnt_day3", "repay_daily_max_cnt_day7", "repay_daily_max_cnt_day14", "repay_daily_max_cnt_day30",
"repay_daily_max_cnt_day60", "repay_daily_max_cnt_day90", "repay_daily_max_cnt_day180", "repay_daily_max_cnt_day240",
"repay_daily_max_cnt_day365", "repay_daily_max_cnt_day730",
"repay_daily_max_org_cnt_day3", "repay_daily_max_org_cnt_day7", "repay_daily_max_org_cnt_day14", "repay_daily_max_org_cnt_day30",
"repay_daily_max_org_cnt_day60", "repay_daily_max_org_cnt_day90", "repay_daily_max_org_cnt_day180", "repay_daily_max_org_cnt_day240",
"repay_daily_max_org_cnt_day365", "repay_daily_max_org_cnt_day730",
"repay_has_record_day_cnt_day3", "repay_has_record_day_cnt_day7", "repay_has_record_day_cnt_day14", "repay_has_record_day_cnt_day30",
"repay_has_record_day_cnt_day60", "repay_has_record_day_cnt_day90", "repay_has_record_day_cnt_day180", "repay_has_record_day_cnt_day240",
"repay_has_record_day_cnt_day365", "repay_has_record_day_cnt_day730",
"repay_max_org_cnt_rate_day3", "repay_max_org_cnt_rate_day7", "repay_max_org_cnt_rate_day14", "repay_max_org_cnt_rate_day30",
"repay_max_org_cnt_rate_day60", "repay_max_org_cnt_rate_day90", "repay_max_org_cnt_rate_day180", "repay_max_org_cnt_rate_day240",
"repay_max_org_cnt_rate_day365", "repay_max_org_cnt_rate_day730",
"repay_mean_delta_day3", "repay_mean_delta_day7", "repay_mean_delta_day14", "repay_mean_delta_day30",
"repay_mean_delta_day60", "repay_mean_delta_day90", "repay_mean_delta_day180", "repay_mean_delta_day240",
"repay_mean_delta_day365", "repay_mean_delta_day730",
"repay_max_delta_day3", "repay_max_delta_day7", "repay_max_delta_day14", "repay_max_delta_day30",
"repay_max_delta_day60", "repay_max_delta_day90", "repay_max_delta_day180", "repay_max_delta_day240",
"repay_max_delta_day365", "repay_max_delta_day730",
"repay_min_delta_day3", "repay_min_delta_day7", "repay_min_delta_day14", "repay_min_delta_day30",
"repay_min_delta_day60", "repay_min_delta_day90", "repay_min_delta_day180", "repay_min_delta_day240",
"repay_min_delta_day365", "repay_min_delta_day730",
"repay_total_amt_day3", "repay_total_amt_day7", "repay_total_amt_day14", "repay_total_amt_day30",
"repay_total_amt_day60", "repay_total_amt_day90", "repay_total_amt_day180", "repay_total_amt_day240",
"repay_total_amt_day365", "repay_total_amt_day730",
"repay_avg_amt_day3", "repay_avg_amt_day7", "repay_avg_amt_day14", "repay_avg_amt_day30",
"repay_avg_amt_day60", "repay_avg_amt_day90", "repay_avg_amt_day180", "repay_avg_amt_day240",
"repay_avg_amt_day365", "repay_avg_amt_day730",
"repay_min_amt_day3", "repay_min_amt_day7", "repay_min_amt_day14", "repay_min_amt_day30",
"repay_min_amt_day60", "repay_min_amt_day90", "repay_min_amt_day180", "repay_min_amt_day240",
"repay_min_amt_day365", "repay_min_amt_day730",
"repay_max_amt_day3", "repay_max_amt_day7", "repay_max_amt_day14", "repay_max_amt_day30",
"repay_max_amt_day60", "repay_max_amt_day90", "repay_max_amt_day180", "repay_max_amt_day240",
"repay_max_amt_day365", "repay_max_amt_day730",
"balance_amt_day3", "balance_amt_day7", "balance_amt_day14", "balance_amt_day30",
"balance_amt_day60", "balance_amt_day90", "balance_amt_day180", "balance_amt_day240",
"balance_amt_day365", "balance_amt_day730",
"balance_amt_rate_day3", "balance_amt_rate_day7", "balance_amt_rate_day14", "balance_amt_rate_day30",
"balance_amt_rate_day60", "balance_amt_rate_day90", "balance_amt_rate_day180", "balance_amt_rate_day240",
"balance_amt_rate_day365", "balance_amt_rate_day730"
),
"time_structure", Set.of(
"apply_days_since_last", "credit_days_since_last", "repay_days_since_last",
"apply_days_since_first", "credit_days_since_first", "repay_days_since_first",
"reject_cnt_day3", "reject_cnt_day7", "reject_cnt_day14", "reject_cnt_day30",
"reject_cnt_day60", "reject_cnt_day90", "reject_cnt_day180", "reject_cnt_day240",
"reject_cnt_day365", "reject_cnt_day730",
"reject_cnt_rate_day3", "reject_cnt_rate_day7", "reject_cnt_rate_day14", "reject_cnt_rate_day30",
"reject_cnt_rate_day60", "reject_cnt_rate_day90", "reject_cnt_rate_day180", "reject_cnt_rate_day240",
"reject_cnt_rate_day365", "reject_cnt_rate_day730",
"reject_org_cnt_day3", "reject_org_cnt_day7", "reject_org_cnt_day14", "reject_org_cnt_day30",
"reject_org_cnt_day60", "reject_org_cnt_day90", "reject_org_cnt_day180", "reject_org_cnt_day240",
"reject_org_cnt_day365", "reject_org_cnt_day730",
"reject_org_cnt_rate_day3", "reject_org_cnt_rate_day7", "reject_org_cnt_rate_day14", "reject_org_cnt_rate_day30",
"reject_org_cnt_rate_day60", "reject_org_cnt_rate_day90", "reject_org_cnt_rate_day180", "reject_org_cnt_rate_day240",
"reject_org_cnt_rate_day365", "reject_org_cnt_rate_day730"
),
"delinq_monitor", Set.of(
"overdue_org_cnt_day3", "overdue_org_cnt_day7", "overdue_org_cnt_day14", "overdue_org_cnt_day30",
"overdue_org_cnt_day60", "overdue_org_cnt_day90", "overdue_org_cnt_day180", "overdue_org_cnt_day240",
"overdue_org_cnt_day365", "overdue_org_cnt_day730",
"overdue_org_cnt_rate_day3", "overdue_org_cnt_rate_day7", "overdue_org_cnt_rate_day14", "overdue_org_cnt_rate_day30",
"overdue_org_cnt_rate_day60", "overdue_org_cnt_rate_day90", "overdue_org_cnt_rate_day180", "overdue_org_cnt_rate_day240",
"overdue_org_cnt_rate_day365", "overdue_org_cnt_rate_day730",
"overdue_failed_pay_cnt_day3", "overdue_failed_pay_cnt_day7", "overdue_failed_pay_cnt_day14", "overdue_failed_pay_cnt_day30",
"overdue_failed_pay_cnt_day60", "overdue_failed_pay_cnt_day90", "overdue_failed_pay_cnt_day180", "overdue_failed_pay_cnt_day240",
"overdue_failed_pay_cnt_day365", "overdue_failed_pay_cnt_day730",
"overdue_failed_pay_org_cnt_day3", "overdue_failed_pay_org_cnt_day7", "overdue_failed_pay_org_cnt_day14", "overdue_failed_pay_org_cnt_day30",
"overdue_failed_pay_org_cnt_day60", "overdue_failed_pay_org_cnt_day90", "overdue_failed_pay_org_cnt_day180", "overdue_failed_pay_org_cnt_day240",
"overdue_failed_pay_org_cnt_day365", "overdue_failed_pay_org_cnt_day730",
"overdue_failed_pay_org_cnt_rate_day3", "overdue_failed_pay_org_cnt_rate_day7", "overdue_failed_pay_org_cnt_rate_day14", "overdue_failed_pay_org_cnt_rate_day30",
"overdue_failed_pay_org_cnt_rate_day60", "overdue_failed_pay_org_cnt_rate_day90", "overdue_failed_pay_org_cnt_rate_day180", "overdue_failed_pay_org_cnt_rate_day240",
"overdue_failed_pay_org_cnt_rate_day365", "overdue_failed_pay_org_cnt_rate_day730"
),
"flow_anomaly", Set.of(
"same_day_pay_repay_cnt_day3", "same_day_pay_repay_cnt_day7", "same_day_pay_repay_cnt_day14", "same_day_pay_repay_cnt_day30",
"same_day_pay_repay_cnt_day60", "same_day_pay_repay_cnt_day90", "same_day_pay_repay_cnt_day180", "same_day_pay_repay_cnt_day240",
"same_day_pay_repay_cnt_day365", "same_day_pay_repay_cnt_day730",
"same_day_pay_repay_daynum_day3", "same_day_pay_repay_daynum_day7", "same_day_pay_repay_daynum_day14", "same_day_pay_repay_daynum_day30",
"same_day_pay_repay_daynum_day60", "same_day_pay_repay_daynum_day90", "same_day_pay_repay_daynum_day180", "same_day_pay_repay_daynum_day240",
"same_day_pay_repay_daynum_day365", "same_day_pay_repay_daynum_day730",
"same_day_pay_repay_rate_day3", "same_day_pay_repay_rate_day7", "same_day_pay_repay_rate_day14", "same_day_pay_repay_rate_day30",
"same_day_pay_repay_rate_day60", "same_day_pay_repay_rate_day90", "same_day_pay_repay_rate_day180", "same_day_pay_repay_rate_day240",
"same_day_pay_repay_rate_day365", "same_day_pay_repay_rate_day730",
"same_day_pay_repay_daynum_rate_day3", "same_day_pay_repay_daynum_rate_day7", "same_day_pay_repay_daynum_rate_day14", "same_day_pay_repay_daynum_rate_day30",
"same_day_pay_repay_daynum_rate_day60", "same_day_pay_repay_daynum_rate_day90", "same_day_pay_repay_daynum_rate_day180", "same_day_pay_repay_daynum_rate_day240",
"same_day_pay_repay_daynum_rate_day365", "same_day_pay_repay_daynum_rate_day730",
"same_day_pay_repay_amt_day3", "same_day_pay_repay_amt_day7", "same_day_pay_repay_amt_day14", "same_day_pay_repay_amt_day30",
"same_day_pay_repay_amt_day60", "same_day_pay_repay_amt_day90", "same_day_pay_repay_amt_day180", "same_day_pay_repay_amt_day240",
"same_day_pay_repay_amt_day365", "same_day_pay_repay_amt_day730",
"same_day_pay_repay_amt_rate_day3", "same_day_pay_repay_amt_rate_day7", "same_day_pay_repay_amt_rate_day14", "same_day_pay_repay_amt_rate_day30",
"same_day_pay_repay_amt_rate_day60", "same_day_pay_repay_amt_rate_day90", "same_day_pay_repay_amt_rate_day180", "same_day_pay_repay_amt_rate_day240",
"same_day_pay_repay_amt_rate_day365", "same_day_pay_repay_amt_rate_day730"
)
);
/** V2.2 相比 V1 新增的字段(不含 overall_score) */
private static final Map<String, Set<String>> V2_ADDITIONS = Map.of(
"loan_behavior", Set.of(
"credit_to_apply_rate_by_org_cnt_day3", "credit_to_apply_rate_by_org_cnt_day7",
"credit_to_apply_rate_by_org_cnt_day14", "credit_to_apply_rate_by_org_cnt_day30",
"credit_to_apply_rate_by_org_cnt_day60", "credit_to_apply_rate_by_org_cnt_day90",
"credit_to_apply_rate_by_org_cnt_day180", "credit_to_apply_rate_by_org_cnt_day240",
"credit_to_apply_rate_by_org_cnt_day365", "credit_to_apply_rate_by_org_cnt_day730"
),
"repay_behavior", Set.of(
"repay_to_credit_rate_by_cnt_day3", "repay_to_credit_rate_by_cnt_day7",
"repay_to_credit_rate_by_cnt_day14", "repay_to_credit_rate_by_cnt_day30",
"repay_to_credit_rate_by_cnt_day60", "repay_to_credit_rate_by_cnt_day90",
"repay_to_credit_rate_by_cnt_day180", "repay_to_credit_rate_by_cnt_day240",
"repay_to_credit_rate_by_cnt_day365", "repay_to_credit_rate_by_cnt_day730",
"repay_to_credit_rate_by_org_cnt_day3", "repay_to_credit_rate_by_org_cnt_day7",
"repay_to_credit_rate_by_org_cnt_day14", "repay_to_credit_rate_by_org_cnt_day30",
"repay_to_credit_rate_by_org_cnt_day60", "repay_to_credit_rate_by_org_cnt_day90",
"repay_to_credit_rate_by_org_cnt_day180", "repay_to_credit_rate_by_org_cnt_day240",
"repay_to_credit_rate_by_org_cnt_day365", "repay_to_credit_rate_by_org_cnt_day730",
"repay_to_apply_rate_by_cnt_day3", "repay_to_apply_rate_by_cnt_day7",
"repay_to_apply_rate_by_cnt_day14", "repay_to_apply_rate_by_cnt_day30",
"repay_to_apply_rate_by_cnt_day60", "repay_to_apply_rate_by_cnt_day90",
"repay_to_apply_rate_by_cnt_day180", "repay_to_apply_rate_by_cnt_day240",
"repay_to_apply_rate_by_cnt_day365", "repay_to_apply_rate_by_cnt_day730",
"repay_to_apply_rate_by_org_cnt_day3", "repay_to_apply_rate_by_org_cnt_day7",
"repay_to_apply_rate_by_org_cnt_day14", "repay_to_apply_rate_by_org_cnt_day30",
"repay_to_apply_rate_by_org_cnt_day60", "repay_to_apply_rate_by_org_cnt_day90",
"repay_to_apply_rate_by_org_cnt_day180", "repay_to_apply_rate_by_org_cnt_day240",
"repay_to_apply_rate_by_org_cnt_day365", "repay_to_apply_rate_by_org_cnt_day730"
),
"delinq_monitor", Set.of(
"overdue_to_apply_rate_by_org_cnt_day3", "overdue_to_apply_rate_by_org_cnt_day7",
"overdue_to_apply_rate_by_org_cnt_day14", "overdue_to_apply_rate_by_org_cnt_day30",
"overdue_to_apply_rate_by_org_cnt_day60", "overdue_to_apply_rate_by_org_cnt_day90",
"overdue_to_apply_rate_by_org_cnt_day180", "overdue_to_apply_rate_by_org_cnt_day240",
"overdue_to_apply_rate_by_org_cnt_day365", "overdue_to_apply_rate_by_org_cnt_day730",
"overdue_to_repay_rate_by_org_cnt_day3", "overdue_to_repay_rate_by_org_cnt_day7",
"overdue_to_repay_rate_by_org_cnt_day14", "overdue_to_repay_rate_by_org_cnt_day30",
"overdue_to_repay_rate_by_org_cnt_day60", "overdue_to_repay_rate_by_org_cnt_day90",
"overdue_to_repay_rate_by_org_cnt_day180", "overdue_to_repay_rate_by_org_cnt_day240",
"overdue_to_repay_rate_by_org_cnt_day365", "overdue_to_repay_rate_by_org_cnt_day730"
)
);
/** V2.2 overall_score 模块全量字段 */
private static final Map<String, Set<String>> SCORE_WHITELIST = Map.of(
"overall_score", Set.of(
"recent_risk_level_v1", "recent_risk_level_v2", "recent_risk_level_v3",
"recent_risk_level_v4", "recent_risk_level_v5",
"mid_risk_level_v1", "mid_risk_level_v2", "mid_risk_level_v3",
"mid_risk_level_v4", "mid_risk_level_v5",
"long_risk_level_v1", "long_risk_level_v2", "long_risk_level_v3",
"long_risk_level_v4", "long_risk_level_v5"
)
);
/** V2_FEATURE 白名单 = V1 + V2_ADDITIONS,懒加载 */
private static volatile Map<String, Set<String>> v2FeatureWhitelist;
private ResultFieldFilter() {
}
public static void filter(Map<String, Object> result, ResultFilterType type) {
if (result == null || result.isEmpty()) {
return;
}
switch (type) {
case V1_FEATURE:
filterByWhitelist(result, V1_WHITELIST);
break;
case V2_FEATURE:
filterByWhitelist(result, getV2FeatureWhitelist());
break;
case V2_SCORE:
filterByWhitelist(result, SCORE_WHITELIST);
break;
}
}
private static void filterByWhitelist(Map<String, Object> result, Map<String, Set<String>> whitelist) {
result.entrySet().removeIf(entry -> !whitelist.containsKey(entry.getKey()));
for (Map.Entry<String, Object> entry : result.entrySet()) {
Set<String> allowedFields = whitelist.get(entry.getKey());
if (allowedFields != null && entry.getValue() instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> fields = (Map<String, Object>) entry.getValue();
fields.keySet().retainAll(allowedFields);
}
}
}
private static Map<String, Set<String>> getV2FeatureWhitelist() {
if (v2FeatureWhitelist == null) {
synchronized (ResultFieldFilter.class) {
if (v2FeatureWhitelist == null) {
Map<String, Set<String>> merged = new HashMap<>(V1_WHITELIST);
V2_ADDITIONS.forEach((module, additions) ->
merged.computeIfPresent(module, (k, existing) -> {
Set<String> combined = new java.util.LinkedHashSet<>(existing);
combined.addAll(additions);
return Set.copyOf(combined);
})
);
v2FeatureWhitelist = Map.copyOf(merged);
}
}
}
return v2FeatureWhitelist;
}
}

View File

@ -1,29 +1,40 @@
# 印尼支付行为画像查询接口
## 接口信息
## 接口概览
| 接口 | 路径 | 说明 |
|---|---|---|
| V1 特征 | `POST /api/v1/fortispay/payment-feature` | 返回 V1 版 536 个特征字段(6 模块) |
| V2 特征 | `POST /api/v1/fortispay/payment-feature-v2` | 返回 V2.2 版 606 个特征字段(6 模块,不含综合评分) |
| V2 评分 | `POST /api/v1/fortispay/payment-feature-v2-score` | 返回 V2.2 版 15 个综合评分字段(overall_score 模块) |
三个接口调用**同一三方 API**,入参、缓存、日志相同,仅响应 `data.result` 字段过滤规则不同。
---
## 公共信息
| 项目 | 说明 |
|---|---|
| 接口路径 | `POST /api/v1/fortispay/payment-feature` |
| 请求格式 | `application/json; charset=UTF-8` |
| 响应格式 | `application/json` |
## 请求头部
### 请求头部
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|------------------------------|
|---|---|---|---|
| Content-Type | String | Y | `application/json` |
| X-Request-Id | String | N | 网关请求唯一标识ID,用于全链路追踪,原样回显在响应头中 |
| X-Language | String | N | 语言标识,传 `en` 返回英文结果,不传默认英文 |
## 响应头部
### 响应头部
| 参数 | 类型 | 说明 |
|---|---|---|
| X-Request-Id | String | 回显请求头中的 X-Request-Id,未传则自动生成 32 位 UUID |
| X-Session-Id | String | 三方服务返回的唯一会话标识(Fortispay responseId) |
## 请求体
### 请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
@ -55,57 +66,9 @@
> id_card、mobile 传密文时必须传 encrypt_algo="MD5"。
## 响应体
### 错误响应
服务透传三方接口原始响应,不做字段裁剪。
| 字段 | 类型 | 说明 |
|---|---|-------------------------------------------|
| code | String | 响应码,`"0"` 表示成功,更多请查看富通接口规范.pdf 常见响应编码表 |
| message | String | 提示消息,如 `"success"` |
| responseId | String | 三方响应的唯一标识ID |
| data | Object | 数据节点,包含 status 和 result |
| data.status | Integer | 查询状态,仅code为 `"0"` 时有值(`1` = 查得,`0` = 未查得) |
| data.result | Object | 结果数据(Map),三方返回的原始 result 对象 |
### 成功响应示例
```json
{
"code": "0",
"message": "success",
"responseId": "0b5a09b6ea7d77dbbbb92e05dc9744ad",
"data": {
"status": 1,
"result": {
"app_behavior": { "..." : "..." },
"loan_behavior": { "..." : "..." },
"repay_behavior": { "..." : "..." },
"time_structure": { "..." : "..." },
"delinq_monitor": { "..." : "..." },
"flow_anomaly": { "..." : "..." }
}
}
}
```
### 未查得响应示例
```json
{
"code": "0",
"message": "success",
"responseId": "0b5a09b6ea7d77dbbbb92e05dc9744ad",
"data": {
"status": 0,
"result": null
}
}
```
## 错误响应
错误响应与三方接口响应保持相同结构,其中`data` 中 `status=0`、`result=null`:
错误响应与三方接口响应保持相同结构,其中 `data` 中 `status=0`、`result=null`:
```json
{
@ -126,7 +89,26 @@
| 503 | 503 | 三方 API 网络不可达 |
| 500 | 500 | 服务内部未知异常 |
## 完整调用示例
---
## 一、V1 特征接口
`POST /api/v1/fortispay/payment-feature`
### 响应说明
`data.result` 只保留 V1.xlsx 中的 536 个字段,6 个模块:
| 模块 | 字段数 | 说明 |
|---|---|---|
| `app_behavior` | 90 | 申请行为变量 |
| `loan_behavior` | 140 | 放款行为变量 |
| `repay_behavior` | 150 | 还款行为变量 |
| `time_structure` | 46 | 行为时间结构变量 |
| `delinq_monitor` | 50 | 逾期行为监控 |
| `flow_anomaly` | 60 | 资金流量异常监控变量 |
### 完整调用示例
```bash
curl -X POST http://{host}:{port}/api/v1/fortispay/payment-feature \
@ -139,3 +121,106 @@ curl -X POST http://{host}:{port}/api/v1/fortispay/payment-feature \
"mobile": "6285887456612"
}'
```
---
## 二、V2 特征接口
`POST /api/v1/fortispay/payment-feature-v2`
### 响应说明
`data.result` 保留 V2.2.xlsx 中的 606 个特征字段(6 模块,不含综合评分):
| 模块 | 字段数 | 相比 V1 新增 |
|---|---|---|
| `app_behavior` | 90 | — |
| `loan_behavior` | 150 | +10(`credit_to_apply_rate_by_org_cnt_day*`) |
| `repay_behavior` | 190 | +40(还款与放款/申请的交叉比率) |
| `time_structure` | 46 | — |
| `delinq_monitor` | 70 | +20(逾期与申请/还款的交叉比率) |
| `flow_anomaly` | 60 | — |
> 注意:比率类字段类型为 `float`(V1 中为 `Int`)。
### 完整调用示例
```bash
curl -X POST http://{host}:{port}/api/v1/fortispay/payment-feature-v2 \
-H "Content-Type: application/json" \
-H "X-Request-Id: 9c6ccc0f3ad3886dfe56ed364b6dc3e8" \
-H "X-Language: en" \
-d '{
"name": "ABCXYZ",
"id_card": "3201190108990002",
"mobile": "6285887456612"
}'
```
---
## 三、V2 评分接口
`POST /api/v1/fortispay/payment-feature-v2-score`
### 响应说明
`data.result` 只保留 V2.2.xlsx 中 `overall_score` 模块的 15 个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| `recent_risk_level_v1` ~ `v5` | Int | 申请人即时风险资质等级 V1~V5 |
| `mid_risk_level_v1` ~ `v5` | Int | 申请人阶段风险资质等级 V1~V5 |
| `long_risk_level_v1` ~ `v5` | Int | 申请人长效风险资质等级 V1~V5 |
### 成功响应示例
```json
{
"code": "0",
"message": "success",
"responseId": "0b5a09b6ea7d77dbbbb92e05dc9744ad",
"data": {
"status": 1,
"result": {
"overall_score": {
"recent_risk_level_v1": 7,
"recent_risk_level_v2": 70,
"recent_risk_level_v3": 8,
"recent_risk_level_v4": 18,
"recent_risk_level_v5": 35,
"mid_risk_level_v1": 46,
"mid_risk_level_v2": 47,
"mid_risk_level_v3": 30,
"mid_risk_level_v4": 87,
"mid_risk_level_v5": 51,
"long_risk_level_v1": 36,
"long_risk_level_v2": 37,
"long_risk_level_v3": 13,
"long_risk_level_v4": 16,
"long_risk_level_v5": 89
}
}
}
}
```
### 完整调用示例
```bash
curl -X POST http://{host}:{port}/api/v1/fortispay/payment-feature-v2-score \
-H "Content-Type: application/json" \
-H "X-Request-Id: 9c6ccc0f3ad3886dfe56ed364b6dc3e8" \
-H "X-Language: en" \
-d '{
"name": "ABCXYZ",
"id_card": "3201190108990002",
"mobile": "6285887456612"
}'
```
---
## 缓存说明
三个接口共享同一套缓存。缓存以原始三方响应存储,命中缓存后按接口版本过滤 `result` 字段返回。同一用户查询任意接口后,其他接口命中缓存均无需再次调用三方。