前端全栈必读:使用 Node.js Crypto 处理天远API加密通信与 KV 数据重组

38 阅读6分钟

一、在 BFF 层重塑风控数据结构

在现代金融 SaaS 平台或信贷管理后台的开发中,前端往往需要展示一个可视化的“借款人风险仪表盘”。然而,上游风控接口为了追求传输效率和扩展性,通常返回扁平化的数据结构。

天远API 的“综合多头”接口(JRZQ8F7C)就是一个典型例子。它一次性聚合了 多头借贷逾期黑名单团伙欺诈 等五大类指标,返回一个包含数百个对象的 KV 数组。如果直接将这个数组透传给前端,前端逻辑会变得异常臃肿。

本文将演示如何在 Node.js 中间件层高效接入此接口。我们将利用 crypto 模块实现 AES-128-CBC 加密通信,并编写一个智能的 Data Transformer,将扁平的 riskCode 数组清洗为层级分明的业务对象,为前端提供“开箱即用”的 JSON 数据。

二、API接口调用示例(Node.js版)

1. 接口技术规范

  • 接口地址https://api.tianyuanapi.com/api/v1/JRZQ8F7C
  • HTTP 方法:POST
  • 鉴权:Header 需携带 Access-Id
  • Payload:Body 中的 data 字段为 AES 加密并 Base64 编码后的字符串。
  • 必填参authorized: "1" (必须获得用户授权)。

2. Node.js 完整实现代码

本示例包含 AES 加解密工具,以及一个核心的 transformData 函数,用于将原始数据分类重组。

JavaScript

const axios = require('axios');
const crypto = require('crypto');

// 配置信息
const CONFIG = {
    apiUrl: '<https://api.tianyuanapi.com/api/v1/JRZQ8F7C>',
    accessId: 'YOUR_ACCESS_ID',      
    accessKey: 'YOUR_ACCESS_KEY_HEX' // 16字节Hex字符串
};

class ComprehensiveRiskService {
    constructor() {
        this.key = Buffer.from(CONFIG.accessKey, 'utf-8').slice(0, 16);
        this.algorithm = 'aes-128-cbc';
    }

    // --- AES 加密 (AES-128-CBC + PKCS7) ---
    encrypt(dataObj) {
        try {
            const iv = crypto.randomBytes(16);
            const plaintext = JSON.stringify(dataObj);
            
            const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
            let encrypted = cipher.update(plaintext, 'utf8', 'base64');
            encrypted += cipher.final('base64');
            
            // 拼接 IV (16bytes) + 密文 -> Base64
            const ivBuffer = iv;
            const encryptedBuffer = Buffer.from(encrypted, 'base64');
            const combinedBuffer = Buffer.concat([ivBuffer, encryptedBuffer]);
            
            return combinedBuffer.toString('base64');
        } catch (err) {
            console.error('Encrypt Error:', err.message);
            return null;
        }
    }

    // --- AES 解密 ---
    decrypt(base64Str) {
        try {
            const combinedBuffer = Buffer.from(base64Str, 'base64');
            const iv = combinedBuffer.slice(0, 16);       
            const content = combinedBuffer.slice(16);     
            
            const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
            let decrypted = decipher.update(content, 'base64', 'utf8');
            decrypted += decipher.final('utf8');
            
            return JSON.parse(decrypted);
        } catch (err) {
            console.error('Decrypt Error:', err.message);
            return null;
        }
    }

    // --- 核心:数据清洗 Transformer ---
    // 将扁平的 List<KV> 转换为层级化的 JSON 对象
    structureRiskData(flatList) {
        const result = {
            score: {},      // 41xxx 评分
            overdue: {},    // 17xxx 逾期
            fraud: {},      // 2xxxx/3xxxx 欺诈
            application: {} // 40xxx 申请
        };

        flatList.forEach(item => {
            const code = String(item.riskCode);
            const value = item.riskCodeValue;

            if (code.startsWith('41')) {
                result.score[code] = value;
            } else if (code.startsWith('17')) {
                result.overdue[code] = value;
            } else if (code.startsWith('2') || code.startsWith('3')) {
                result.fraud[code] = value;
            } else if (code.startsWith('40')) {
                result.application[code] = value;
            }
        });
        return result;
    }

    // --- 业务调用主流程 ---
    async queryRiskProfile(userParams) {
        // 1. 补充必填的授权字段
        const payload = { ...userParams, authorized: "1" };
        
        const encryptedData = this.encrypt(payload);
        if (!encryptedData) return;

        try {
            const url = `${CONFIG.apiUrl}?t=${Date.now()}`;
            const res = await axios.post(url, { data: encryptedData }, {
                headers: { 
                    'Content-Type': 'application/json',
                    'Access-Id': CONFIG.accessId 
                }
            });

            if (res.data.code === '200' || res.data.code === 200) {
                console.log('API调用成功,开始清洗数据...');
                
                // 解密数据(假设返回的是加密串,部分情况可能直接返回数组,视实际响应而定)
                // 此处按标准加密流程处理
                let rawList = [];
                if (typeof res.data.data === 'string') {
                    rawList = this.decrypt(res.data.data);
                } else {
                    rawList = res.data.data;
                }

                // 2. 执行数据重组
                const structuredData = this.structureRiskData(rawList);
                return structuredData;
            } else {
                console.error(`API业务错误: ${res.data.message} (Code: ${res.data.code})`);
                return null;
            }
        } catch (error) {
            console.error('网络请求失败:', error.message);
        }
    }
}

// === 使用示例 ===
(async () => {
    const service = new ComprehensiveRiskService();
    
    const profile = await service.queryRiskProfile({
        name: "李四",
        id_card: "110101199001011234",
        mobile_no: "13800138000"
    });

    if (profile) {
        console.log("=== 结构化风控报告 ===");
        
        // 直接访问重组后的对象,逻辑更清晰
        const overdueCount = profile.overdue['17001'] || 0; // 1周内逾期数
        const fraudLevel = profile.fraud['22006'] || 1;     // 圈团风险等级
        
        if (Number(overdueCount) > 0) {
            console.warn(`[拦截] 检测到当前存在 ${overdueCount} 个逾期平台!`);
        } else if (Number(fraudLevel) === 3) {
            console.warn(`[拦截] 检测到高风险欺诈团伙关联!`);
        } else {
            console.log(`[通过] 信用评分: ${profile.score['41001']}`);
        }
    }
})();

三、核心数据结构解析

本接口的数据量极大,包含五大类指标。Node.js 开发者需要理解原始数据与业务数据的映射关系。

1. 原始数据 (Flat List)

接口返回的是为了传输优化的数组:

JSON

[  { "riskCode": 17001, "riskCodeValue": 2 },  { "riskCode": 41001, "riskCodeValue": 85 },  { "riskCode": 22006, "riskCodeValue": 3 }]

2. 清洗后数据 (Nested Object)

经过 structureRiskData 函数处理后,数据变得语义化,适合前端组件绑定:

JavaScript

{
  "overdue": {
    "17001": 2  // 1周内逾期平台数
  },
  "score": {
    "41001": 85 // 多头通用分
  },
  "fraud": {
    "22006": 3  // 团伙风险等级
  }
}

四、字段详解(Node.js 开发速查)

以下表格精选了综合版接口中新增且最具价值的字段,Node.js 开发者应重点关注这些字段的透传。

  1. 逾期风险 (Overdue) - 3

这是本接口的核心增量数据,直接反映用户的违约历史。

Code字段含义说明Node.js 逻辑建议
170011周内逾期平台数当前正在逾期若 > 0,BFF 层直接返回 status: REJECT
170033个月内逾期平台数近期违约迹象结合申请次数展示趋势图。
170061年以前逾期平台数历史黑名单用于标记“有过违约记录的老户”。
  1. 欺诈风险 (Fraud) - 4

无需对接复杂的图数据库,直接获取关联网络分析结果。

Code字段含义值域Node.js 逻辑建议
22006圈团风险等级1(低), 2(中), 3(高)3 = 高危,建议标记为红色预警。
31006疑似准入风险1(低), 2(中), 3(高)基于资料虚假的判定,建议拦截高风险。
21007圈团浓度分0-100分数越高,周围关联的黑产越多。

五、应用价值分析

在 Node.js 驱动的架构中,集成天远综合多头API主要服务于以下场景:

  1. Dashboard 数据聚合层:

    前端(React/Vue)通常需要展示“信用分”、“近7天申请趋势”、“是否存在逾期”等模块。Node.js BFF 层调用一次本接口,通过 structureRiskData 将数据拆解分发给前端的各个组件,避免了前端进行复杂的数组遍历。

  2. API 网关的“守门员”:

    在请求到达核心 Java/Go 服务之前,Node.js 网关可以先调用此接口进行“初筛”。

    • 快速失败 (Fail-Fast) :如果 17001 (当前逾期) > 0,网关直接拒绝请求,无需浪费后端昂贵的算力资源。
    • 流量分发:根据 41005 (银行分) 的高低,将用户引导至不同的贷款产品页面。
  3. 实时反欺诈 (Real-time Anti-Fraud):

    利用 22006 (圈团风险) 指标。如果用户被识别为高风险团伙成员(CodeValue=3),Node.js 服务可以立即触发设备指纹记录或短信验证码拦截,防止大规模机器攻击。

六、总结

天远综合多头 API 提供了目前信贷风控中最全的数据维度。对于 Node.js 开发者,核心任务不仅仅是 AES 解密,更重要的是数据治理

通过本文提供的 Transformer 模式,您可以将杂乱的 KV 列表转化为结构清晰的业务对象,不仅提升了代码的可维护性,也为前端提供了更加友好的数据接口。建议在 BFF 层对 17xxx (逾期) 和 2xxxx (欺诈) 类指标做强校验,守好风控的第一道防线。