diff --git a/src/main/java/com/lyxh/api/docking/controller/ftapi/FortispayController.java b/src/main/java/com/lyxh/api/docking/controller/ftapi/FortispayController.java index 49a9d9b..567e444 100644 --- a/src/main/java/com/lyxh/api/docking/controller/ftapi/FortispayController.java +++ b/src/main/java/com/lyxh/api/docking/controller/ftapi/FortispayController.java @@ -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 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 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 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 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()) { diff --git a/src/main/java/com/lyxh/api/docking/enums/ResultFilterType.java b/src/main/java/com/lyxh/api/docking/enums/ResultFilterType.java new file mode 100644 index 0000000..b1871fb --- /dev/null +++ b/src/main/java/com/lyxh/api/docking/enums/ResultFilterType.java @@ -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 +} diff --git a/src/main/java/com/lyxh/api/docking/service/PaymentProfileService.java b/src/main/java/com/lyxh/api/docking/service/PaymentProfileService.java index 9a1aff3..f8dde70 100644 --- a/src/main/java/com/lyxh/api/docking/service/PaymentProfileService.java +++ b/src/main/java/com/lyxh/api/docking/service/PaymentProfileService.java @@ -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 resultMap = objectMapper.convertValue(result, Map.class); + ResultFieldFilter.filter(resultMap, filterType); dataNode.setResult(resultMap); } response.setData(dataNode); diff --git a/src/main/java/com/lyxh/api/docking/util/ResultFieldFilter.java b/src/main/java/com/lyxh/api/docking/util/ResultFieldFilter.java new file mode 100644 index 0000000..c8c7496 --- /dev/null +++ b/src/main/java/com/lyxh/api/docking/util/ResultFieldFilter.java @@ -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> 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> 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> 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> v2FeatureWhitelist; + + private ResultFieldFilter() { + } + + public static void filter(Map 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 result, Map> whitelist) { + result.entrySet().removeIf(entry -> !whitelist.containsKey(entry.getKey())); + for (Map.Entry entry : result.entrySet()) { + Set allowedFields = whitelist.get(entry.getKey()); + if (allowedFields != null && entry.getValue() instanceof Map) { + @SuppressWarnings("unchecked") + Map fields = (Map) entry.getValue(); + fields.keySet().retainAll(allowedFields); + } + } + } + + private static Map> getV2FeatureWhitelist() { + if (v2FeatureWhitelist == null) { + synchronized (ResultFieldFilter.class) { + if (v2FeatureWhitelist == null) { + Map> merged = new HashMap<>(V1_WHITELIST); + V2_ADDITIONS.forEach((module, additions) -> + merged.computeIfPresent(module, (k, existing) -> { + Set combined = new java.util.LinkedHashSet<>(existing); + combined.addAll(additions); + return Set.copyOf(combined); + }) + ); + v2FeatureWhitelist = Map.copyOf(merged); + } + } + } + return v2FeatureWhitelist; + } +} diff --git a/对接接口文档.md b/对接接口文档.md index f9c7e23..0b1ea88 100644 --- a/对接接口文档.md +++ b/对接接口文档.md @@ -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` | +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| Content-Type | String | Y | `application/json` | | X-Request-Id | String | N | 网关请求唯一标识ID,用于全链路追踪,原样回显在响应头中 | -| X-Language | String | N | 语言标识,传 `en` 返回英文结果,不传默认英文 | +| 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` 字段返回。同一用户查询任意接口后,其他接口命中缓存均无需再次调用三方。