Node.js全栈实战:构建基于天远多头借贷行业风险版API的BFF风控层

解决风控数据"最后一公里"的传输难题

在开发贷超导流页、信用卡申请 H5 或消费分期小程序时,前端面临着巨大的挑战:业务需要实时展示用户的信用评估状态(如"审核中"、"极速放款"或"暂不符合"),但直接将包含数百个敏感指标的 天远多头借贷行业风险版 数据暴露给前端是非常危险的。此外,API 返回的 List<RiskItem> 结构虽然灵活,但对于前端 UI 渲染(如仪表盘、进度条)并不友好。

通过 Node.js 搭建一个轻量级的 BFF 层,我们可以完美解决这些问题。Node.js 不仅负责处理 AES 加密通信,保护 Access Key 不泄露,还能充当"数据适配器",将天远 API 返回的 41001(通用分)、17001(逾期数)等晦涩代码,转换为前端易读的 JSON 对象,从而实现业务逻辑与 UI 展现的解耦。

Node.js 中间件集成实战

本节演示如何使用 Node.js(Express/Koa 风格)构建一个安全的代理服务。该服务实现了对 天远多头借贷行业风险版 接口的加密调用,并内置了数据清洗逻辑。

开发环境配置

  • 运行环境: Node.js (v14+)
  • 依赖库 : axios (HTTP请求), crypto (原生加密模块)
  • 接口地址 : https://api.tianyuanapi.com/api/v1/DWBG7F3A

核心代码实现

JavaScript

jsx 复制代码
const axios = require('axios');
const crypto = require('crypto');

// 环境变量配置
const CONFIG = {
    apiUrl: 'https://api.tianyuanapi.com/api/v1/DWBG7F3A',
    accessId: process.env.TIANYUAN_ACCESS_ID || 'YOUR_ACCESS_ID',
    accessKey: process.env.TIANYUAN_ACCESS_KEY || 'YOUR_ACCESS_KEY_HEX' // 16进制字符串
};

/**
 * 核心加密函数:AES-128-CBC + PKCS7 + Random IV + Base64
 */
function encryptData(payload, keyHex) {
    const key = Buffer.from(keyHex, 'utf8'); // 确保Key编码一致
    const iv = crypto.randomBytes(16);       // 生成随机16字节IV
    const plaintext = JSON.stringify(payload);

    const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
    let encrypted = cipher.update(plaintext, 'utf8');
    encrypted = Buffer.concat([encrypted, cipher.final()]);

    // 拼接 IV + 密文 -> Base64
    const combined = Buffer.concat([iv, encrypted]);
    return combined.toString('base64');
}

/**
 * 核心解密函数
 */
function decryptData(encryptedBase64, keyHex) {
    const key = Buffer.from(keyHex, 'utf8');
    const combined = Buffer.from(encryptedBase64, 'base64');

    const iv = combined.slice(0, 16);
    const ciphertext = combined.slice(16);

    const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
    let decrypted = decipher.update(ciphertext);
    decrypted = Buffer.concat([decrypted, decipher.final()]);

    return JSON.parse(decrypted.toString('utf8'));
}

/**
 * 数据适配器:将 API 的 List 结构转换为前端友好的 Map
 * 示例:[{"riskCode": "41001", "riskCodeValue": "50"}] -> { generalScore: 50 }
 */
function transformRiskData(rawReport) {
    if (!rawReport || !Array.isArray(rawReport)) return {};

    // 定义前端关注的字段映射
    const fieldMap = {
        '41001': 'generalScore',       // 通用分 
        '41002': 'shortTermScore',     // 短周期分 
        '17001': 'overdueCount7d',     // 7天逾期平台数 
        '31006': 'fraudRiskLevel'      // 欺诈风险等级 
    };

    const result = {};
    rawReport.forEach(item => {
        const key = fieldMap[item.riskCode];
        if (key) {
            // 尝试转换为数字,方便前端计算
            result[key] = isNaN(Number(item.riskCodeValue)) ? item.riskCodeValue : Number(item.riskCodeValue);
        }
    });

    return result;
}

/**
 * 业务服务:获取清洗后的风险数据
 */
async function getRiskProfileForFrontend(userParams) {
    try {
        // 1. 构造并加密请求
        const payload = {
            name: userParams.name,
            id_card: userParams.idCard,
            mobile_no: userParams.mobile
        };
        const encryptedData = encryptData(payload, CONFIG.accessKey);

        // 2. 发起请求
        const timestamp = Date.now();
        const response = await axios.post(
            `${CONFIG.apiUrl}?t=${timestamp}`,
            { data: encryptedData },
            {
                headers: { 'Access-Id': CONFIG.accessId },
                timeout: 8000 // 8秒超时
            }
        );

        const resBody = response.data;

        // 3. 处理响应与清洗数据
        if (resBody.code === 0) {
            const rawData = decryptData(resBody.data, CONFIG.accessKey);
            // 提取核心报告列表
            const reportList = rawData['riskInfo_report_v3.1']; // 
            
            // 返回清洗后的数据给前端
            return {
                status: 'success',
                data: transformRiskData(reportList)
            };
        } else {
            console.warn(`API Error: ${resBody.code}`);
            return { status: 'error', message: 'Risk check failed' };
        }

    } catch (error) {
        console.error('System Error:', error.message);
        throw error;
    }
}

// 模拟 Controller 调用
getRiskProfileForFrontend({
    name: '赵六',
    idCard: '4401011990xxxx',
    mobile: '1380000xxxx'
}).then(console.log);

数据结构的前端映射策略

API 返回的原始数据是基于 RiskCode 的列表,这对于后端存储很方便,但对于前端渲染组件(如 Vue/React 组件)则显得繁琐。BFF 层应承担起"翻译"的职责。

以下是核心字段的映射建议表:

原始 RiskCode 原始含义 转换后的前端字段 前端 UI 组件建议 业务逻辑 (Developer Notes)
41001 多头申请通用分 creditScore 仪表盘 (Gauge) 0-100分。若分数 > 80,UI 可显示红色预警图标,提示"综合评分过低"。
41002 短周期多头子分 urgentIndex 趋势图 (Trend) 若此值显著高于 41003 (长周期分),说明用户近期急需用钱,前端可引导推荐"小额极速贷"产品。
17001 1周内逾期平台数 hasOverdue 弹窗警告 (Alert) 只要值 > 0,前端应通过 BFF 逻辑直接屏蔽借款按钮,显示"暂不满足申请条件"。
31006 疑似准入风险 securityLevel 徽章 (Badge) 值为 1/2/3。若为 3 (高风险),前端可触发人脸识别强校验流程。

场景化应用:BFF 层的智能路由

利用 Node.js 的灵活性,我们可以在 BFF 层实现比单纯数据转发更高级的功能:

  1. 动态 A/B Testing

    在新品上线阶段,根据 41004 (非银行多头子分) 将用户分流。

    • 策略 :对于非银分较低(借贷习惯良好)的用户,BFF 层返回 flag: 'A',前端展示"大额低息"的新品落地页;对于分数较高的用户,返回 flag: 'B',展示传统的风控兜底页。
  2. 表单的渐进式披露

    为了提高转化率,不需要所有用户都填写复杂的补充信息。

    • 策略 :用户输入三要素后,Node.js 静默调用 API。若 41001 (通用分) 极好,直接跳过"上传工作证明"步骤;若分数处于临界值,服务端下发指令要求前端动态加载"补充资料"表单组件。
  3. 无感知的黑产拦截

    当 21007 (圈团1浓度分) 较高时,说明可能是黑产团伙攻击。

    • 策略:BFF 层不直接报错(避免打草惊蛇),而是返回一个虚假的"排队中"状态,同时在后台触发安全告警,将该 IP 和设备指纹加入黑名单。

结语

通过 Node.js 集成 天远多头借贷行业风险版 API,我们不仅保护了数据安全,更通过 BFF 层实现了风控逻辑的前置化与轻量化。这种架构模式使得前端工程师可以专注于交互体验,而将复杂的加密、清洗和策略判断逻辑封装在中间层,极大地提升了开发效率和系统的可维护性。

相关推荐
RPA机器人就选八爪鱼17 小时前
RPA财务机器人选型攻略:5步搭建高性价比自动化体系
大数据·人工智能·机器人·自动化·rpa
予枫的编程笔记17 小时前
Elasticsearch深度搜索与查询DSL实战:精准定位数据的核心技法
java·大数据·人工智能·elasticsearch·搜索引擎·全文检索
新钛云服17 小时前
Grafana Polystat面板与腾讯云可观测平台的深度融合实践
大数据·云计算·腾讯云·grafana
小北方城市网17 小时前
第 6 课:云原生架构终极落地|K8s 全栈编排与高可用架构设计实战
大数据·人工智能·python·云原生·架构·kubernetes·geo
青主创享阁17 小时前
技术破局农业利润困局:玄晶引擎AI数字化解决方案的架构设计与落地实践
大数据·人工智能
正在走向自律17 小时前
大数据时代时序数据库选型指南:为何Apache IoTDB成为物联网场景首
大数据·时序数据库·apache iotdb
Justice Young17 小时前
Hive第五章:Integeration with HBase
大数据·数据仓库·hive·hbase
天远Date Lab17 小时前
Python金融风控实战:集成天远多头借贷行业风险版API实现共债预警
大数据·python
Justice Young18 小时前
Hive第三章:HQL的使用
大数据·数据仓库·hive·hadoop