diff --git a/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go b/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go index d37c02d..48b0601 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go @@ -42,5 +42,9 @@ func ProcessJRZQ0OO1Request(ctx context.Context, params []byte, deps *processors return nil, errors.Join(processors.ErrSystem, err) } - return respBytes, nil + transformed, err := transformJRZQ0OO1Response(respBytes) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + return transformed, nil } diff --git a/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go new file mode 100644 index 0000000..eda1d38 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go @@ -0,0 +1,70 @@ +package jrzq + +import ( + "encoding/json" + "fmt" + "strings" +) + +// JRZQ0OO1 字段命名约定:{窗口}_{机构类型}_{事件}_{指标} +// 窗口: m1/m3/m6/m12 +// 机构类型: 省略=全部贷款;bank=银行;cfc=消金;sloan=小贷 +// 事件: total/severe_overdue/overdue/repay_fail/repay_ok/repay_remind/ +// +// disburse_fail/disburse_ok/audit_fail/audit_ok/apply/register/verify_code/marketing/other +// +// 指标: cnt=次数;org=机构数 +var ( + jrzq0OO1Periods = []string{"m1", "m3", "m6", "m12"} + jrzq0OO1InstTypes = []string{"", "bank", "cfc", "sloan"} + jrzq0OO1Events = []string{ + "total", "severe_overdue", "overdue", "repay_fail", "repay_ok", + "repay_remind", "disburse_fail", "disburse_ok", "audit_fail", "audit_ok", + "apply", "register", "verify_code", "marketing", "other", + } + jrzq0OO1Metrics = []string{"cnt", "org"} + + // jrzq0OO1FieldMap B001-B480 -> 语义化扁平字段名 + jrzq0OO1FieldMap = buildJRZQ0OO1FieldMap() +) + +func buildJRZQ0OO1FieldMap() map[string]string { + m := make(map[string]string, 480) + n := 1 + for _, period := range jrzq0OO1Periods { + for _, inst := range jrzq0OO1InstTypes { + for _, event := range jrzq0OO1Events { + for _, metric := range jrzq0OO1Metrics { + key := fmt.Sprintf("B%03d", n) + parts := []string{period} + if inst != "" { + parts = append(parts, inst) + } + parts = append(parts, event, metric) + m[key] = strings.Join(parts, "_") + n++ + } + } + } + } + return m +} + +// transformJRZQ0OO1Response 将 Info360 的 Bxxx 字段重命名为语义化扁平字段。 +// 未在映射表中的键原样保留。 +func transformJRZQ0OO1Response(respBytes []byte) ([]byte, error) { + var raw map[string]interface{} + if err := json.Unmarshal(respBytes, &raw); err != nil { + return nil, err + } + + out := make(map[string]interface{}, len(raw)) + for k, v := range raw { + if renamed, ok := jrzq0OO1FieldMap[k]; ok { + out[renamed] = v + continue + } + out[k] = v + } + return json.Marshal(out) +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go new file mode 100644 index 0000000..4e45051 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go @@ -0,0 +1,156 @@ +package jrzq + +import ( + "encoding/json" + "testing" +) + +func TestBuildJRZQ0OO1FieldMap_CountAndBounds(t *testing.T) { + if len(jrzq0OO1FieldMap) != 480 { + t.Fatalf("expected 480 mappings, got %d", len(jrzq0OO1FieldMap)) + } + if got, want := jrzq0OO1FieldMap["B001"], "m1_total_cnt"; got != want { + t.Fatalf("B001: got %q want %q", got, want) + } + if got, want := jrzq0OO1FieldMap["B480"], "m12_sloan_other_org"; got != want { + t.Fatalf("B480: got %q want %q", got, want) + } +} + +func TestJRZQ0OO1FieldMap_DocSpotChecks(t *testing.T) { + cases := map[string]string{ + "B001": "m1_total_cnt", // 最近1个月贷款类总次数 + "B009": "m1_repay_ok_cnt", // 最近1个月还款成功次数 + "B031": "m1_bank_total_cnt", // 最近1个月银行类贷款总次数 + "B129": "m3_repay_ok_cnt", // 最近3个月还款成功次数 + "B130": "m3_repay_ok_org", // 最近3个月还款成功机构数 + "B133": "m3_disburse_fail_cnt", // 最近3个月放款失败次数 + "B134": "m3_disburse_fail_org", // 最近3个月放款失败机构数 + "B157": "m3_bank_repay_fail_cnt", // 最近3个月银行类还款失败次数 + "B158": "m3_bank_repay_fail_org", // 最近3个月银行类还款失败机构数 + "B219": "m3_sloan_repay_ok_cnt", // 最近3个月小贷类还款成功次数 + "B220": "m3_sloan_repay_ok_org", // 最近3个月小贷类还款成功机构数 + "B223": "m3_sloan_disburse_fail_cnt", // 最近3个月小贷类放款失败次数 + "B224": "m3_sloan_disburse_fail_org", // 最近3个月小贷类放款失败机构数 + "B247": "m6_repay_fail_cnt", // 最近6个月还款失败次数 + "B248": "m6_repay_fail_org", // 最近6个月还款失败机构数 + "B257": "m6_audit_fail_cnt", // 最近6个月审核失败次数 + "B258": "m6_audit_fail_org", // 最近6个月审核失败机构数 + "B261": "m6_apply_cnt", // 最近6个月贷款申请次数 + "B262": "m6_apply_org", // 最近6个月贷款申请机构数 + "B285": "m6_bank_disburse_ok_cnt", // 最近6个月银行类放款成功次数 + "B286": "m6_bank_disburse_ok_org", // 最近6个月银行类放款成功机构数 + "B347": "m6_sloan_audit_fail_cnt", // 最近6个月小贷类审核失败次数 + "B348": "m6_sloan_audit_fail_org", // 最近6个月小贷类审核失败机构数 + "B351": "m6_sloan_apply_cnt", // 最近6个月小贷类贷款申请次数 + "B352": "m6_sloan_apply_org", // 最近6个月小贷类贷款申请机构数 + "B375": "m12_disburse_ok_cnt", // 最近12个月放款成功次数 + "B376": "m12_disburse_ok_org", // 最近12个月放款成功机构数 + "B061": "m1_cfc_total_cnt", // 最近1个月持牌消金类贷款总次数 + "B301": "m6_cfc_total_cnt", // 最近6个月消金类贷款总次数 + } + for src, want := range cases { + if got := jrzq0OO1FieldMap[src]; got != want { + t.Errorf("%s: got %q want %q", src, got, want) + } + } +} + +func TestTransformJRZQ0OO1Response_Sample(t *testing.T) { + in := []byte(`{ + "B129": 6, + "B130": 3, + "B133": 4, + "B134": 2, + "B157": 2, + "B158": 1, + "B219": 6, + "B220": 3, + "B223": 4, + "B224": 2, + "B247": 2, + "B248": 1, + "B257": 6, + "B258": 3, + "B261": 4, + "B262": 2, + "B285": 2, + "B286": 1, + "B347": 6, + "B348": 3, + "B351": 4, + "B352": 2, + "B375": 2, + "B376": 1 +}`) + + outBytes, err := transformJRZQ0OO1Response(in) + if err != nil { + t.Fatal(err) + } + + var out map[string]interface{} + if err := json.Unmarshal(outBytes, &out); err != nil { + t.Fatal(err) + } + + want := map[string]float64{ + "m3_repay_ok_cnt": 6, + "m3_repay_ok_org": 3, + "m3_disburse_fail_cnt": 4, + "m3_disburse_fail_org": 2, + "m3_bank_repay_fail_cnt": 2, + "m3_bank_repay_fail_org": 1, + "m3_sloan_repay_ok_cnt": 6, + "m3_sloan_repay_ok_org": 3, + "m3_sloan_disburse_fail_cnt": 4, + "m3_sloan_disburse_fail_org": 2, + "m6_repay_fail_cnt": 2, + "m6_repay_fail_org": 1, + "m6_audit_fail_cnt": 6, + "m6_audit_fail_org": 3, + "m6_apply_cnt": 4, + "m6_apply_org": 2, + "m6_bank_disburse_ok_cnt": 2, + "m6_bank_disburse_ok_org": 1, + "m6_sloan_audit_fail_cnt": 6, + "m6_sloan_audit_fail_org": 3, + "m6_sloan_apply_cnt": 4, + "m6_sloan_apply_org": 2, + "m12_disburse_ok_cnt": 2, + "m12_disburse_ok_org": 1, + } + + if len(out) != len(want) { + t.Fatalf("field count: got %d want %d, out=%v", len(out), len(want), out) + } + for k, w := range want { + got, ok := out[k] + if !ok { + t.Errorf("missing key %s", k) + continue + } + num, ok := got.(float64) + if !ok || num != w { + t.Errorf("%s: got %#v want %v", k, got, w) + } + } +} + +func TestTransformJRZQ0OO1Response_KeepUnknown(t *testing.T) { + in := []byte(`{"B001":1,"extra":"x"}`) + outBytes, err := transformJRZQ0OO1Response(in) + if err != nil { + t.Fatal(err) + } + var out map[string]interface{} + if err := json.Unmarshal(outBytes, &out); err != nil { + t.Fatal(err) + } + if _, ok := out["m1_total_cnt"]; !ok { + t.Fatal("expected m1_total_cnt") + } + if out["extra"] != "x" { + t.Fatalf("expected extra kept, got %#v", out["extra"]) + } +}