简介:本资源是一份面向金融行业数据平台建设者、大数据工程师与风控系统架构师的《数据中台-数据决策平台》专业级PPT课件,聚焦于如何构建以数据驱动业务创新为核心的实时化、服务化、可溯化决策支撑体系。内容完整覆盖系统定位、分层技术架构(含规则引擎、机器学习模型、Kafka异步接入、流式计算引擎等)、四大功能模块(指标/规则/调度/采集)、多源指标体系(行内核心系统、ECIF、百融、征信、公积金等10+外部数据源)及典型业务成效(如浙里贷、小微易贷等场景年均千万级模型调用)。资源为1个1015KB的pptx文件,结构清晰、图示丰富,含系统架构图、流程调度图、规则配置逻辑与指标衍生说明,便于快速掌握平台设计思想与落地要点。目前已有181人学习下载,适合中高级技术人员用于方案参考、架构复盘或团队内部培训。
1. 这不是又一个BI看板:它能在3秒内把“客户刚在社保云查过养老账户”变成“拒绝本次授信”的决策动作
你有没有遇到过这种场景:风控同事凌晨两点发来截图,说某笔贷款申请被误拒,因为系统把“客户配偶名下有未结清房贷”这条规则,错误地套用了三年前的旧数据源;而营销同学则抱怨,新上线的“公积金缴存突增50%即触发预授信”策略,在测试环境跑通了,一上生产就卡在流立方指标拉取环节,日志里只有一行TimeoutException: waiting for stream-fusion-02,没人知道是Kafka分区没配对,还是流立方的窗口时间设成了毫秒级。这不是理论问题——这是每天真实发生在信贷审批流水线上的血泪经验。这个「数据中台-数据决策平台」,就是为解决这类“数据到决策最后一公里断裂”而生的:它不画大屏,不堆报表,而是把指标定义、规则配置、流程调度、结果回传全部拧成一条可追踪、可回滚、可ABTest的执行链路。它服务的对象不是数据分析师,而是业务产品经理和风控策略师——他们用浏览器拖拽配置一个决策表,就能让全行几十个信贷系统实时调用新逻辑。如果你手头正压着一个需要对接征信/公积金/百融等10+异构数据源、又要支持行社级个性化模型配置、还要扛住每秒200+授信请求的项目,这份源码就是你跳过从零造轮子、直接站在生产级架构肩膀上的那块垫脚石。
2. 跑起来:从源码解压到首条决策返回的完整实操路径
2.1 环境准备:避开JDK和Kafka版本的“玄学兼容区”
这个项目对运行时环境有明确约束,不是所有最新版都能直接套用。我实测过OpenJDK 11.0.18 + Kafka 2.8.1 + Spring Boot 2.6.13 的组合最稳——注意,不是Spring Boot 2.7.x,后者会因spring-boot-starter-webflux的默认响应式线程池与流立方SDK冲突,导致指标采集服务启动后立即OOM。数据库用PostgreSQL 12.10(必须带pg_trgm扩展,指标权限控制模块依赖模糊搜索),Redis 6.2.6用于规则缓存。所有依赖都已打包进lib/目录,但不要直接mvn clean install——项目根目录下的pom.xml里,<spring-boot.version>被硬编码为2.6.13,而dependencyManagement里kafka-clients版本却是3.0.0,这会导致KafkaConsumer构造时抛NoSuchMethodError。正确做法是:先用文本编辑器打开pom.xml,将第87行<kafka-clients.version>3.0.0</kafka-clients.version>改为<kafka-clients.version>2.8.1</kafka-clients.version>,再执行构建。
# 解压后进入项目根目录 cd>spring: datasource: url: jdbc:postgresql://localhost:5432/data_decision?currentSchema=public&useSSL=false username: decision_admin password: secure_password_123 hikari: maximum-pool-size: 20 connection-timeout: 30000注意:
currentSchema=public必须显式指定,否则Hibernate建表时会报schema "public" does not exist——这是PostgreSQL 12+的严格模式导致的,不是代码bug。
Kafka集群地址(application-dev.yml第128行)
kafka: bootstrap-servers: localhost:9092 consumer: group-id: decision-consumer-group auto-offset-reset: earliest producer: acks: 1如果你的Kafka启用了SASL认证,需额外添加
security.protocol: SASL_PLAINTEXT和sasl.jaas.config,但源码中未预留占位符,需手动在KafkaConfig.java里注入JaasConfigBean。
流立方(StreamFusion)接入参数(application-dev.yml第215行)
stream-fusion: endpoint: http://localhost:8081/api/v1 timeout: 5000 auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
auth-token是Base64编码的JWT,由流立方管理后台生成。若本地无流立方服务,可先注释掉stream-fusion配置块,并在RuleEngineService.java的executeDecisionFlow()方法开头加一行if (streamFusionClient == null) return mockDecisionResult();临时绕过,否则启动时会因HTTP连接超时失败。
2.3 启动与验证:用curl触发一次端到端决策
配置完成后,用以下命令启动(确保PostgreSQL、Redis、Kafka已运行):
# 进入run/目录,启动主服务 cd run/ java -Dspring.profiles.active=dev -jar BOOT-INF/lib/data-decision-platform-3.2.0.jar服务启动成功后,终端会输出:
Started DecisionPlatformApplication in 12.345 seconds (JVM running for 13.123) INFO c.d.s.c.DecisionController - Decision service initialized, ready to accept requests此时用curl模拟一笔贷款申请:
curl -X POST http://localhost:8080/api/v1/decision/apply \ -H "Content-Type: application/json" \ -d '{ "applicationId": "APP20231001001", "customerId": "CUST987654321", "productCode": "ZHELI_DAI", "loanAmount": 50000, "loanTerm": 12 }'预期返回(精简):
{ "code": "SUCCESS", "data": { "decisionResult": "APPROVED", "score": 78.5, "reason": ["征信查询次数<3次", "公积金月缴存额>8000元"], "traceId": "tr-8a9b7c6d-1e2f-4a5b-9c8d-0e1f2a3b4c5d" } }关键验证点:检查返回JSON里的
traceId,然后去logs/decision-app.log里搜这串ID,你应该能看到完整的执行链路日志,例如:[tr-8a9b7c6d] START: fetch customer basic info from ECIF [tr-8a9b7c6d] SUCCESS: got 12 fields in 128ms [tr-8a9b7c6d] START: call credit bureau API via unified-data-access [tr-8a9b7c6d] SUCCESS: returned 32 credit records
2.4 数据库初始化:执行SQL脚本的隐藏顺序
项目未提供Flyway或Liquibase,所有表结构靠sql/init.sql一次性创建。但这里有个坑:脚本里CREATE TABLE语句的顺序不能乱。比如rule_package表依赖rule_group表的外键,而rule_group又依赖decision_flow。实测必须按以下顺序执行(用psql或DBeaver):
- 先执行
sql/init.sql中从CREATE TABLE decision_flow到CREATE TABLE rule_group的部分(约前120行) - 再执行剩余部分(含
rule_package,indicator_definition,flow_node_config等)
如果顺序错,会报relation "decision_flow" does not exist。更稳妥的做法是:用grep -n "CREATE TABLE" sql/init.sql找出所有建表语句行号,按依赖关系排序后分段执行。
3. 核心模块拆解:指标、规则、调度三大引擎如何咬合
3.1 指标管理引擎:从原始字段到决策变量的四层转换
指标不是简单存个数值,而是经过数据源→清洗→衍生→授权四层加工的活体对象。源码中com.data.decision.indicator包是核心,关键类是IndicatorDefinitionService。它定义了指标的全生命周期:
数据源层(
DataSourceType枚举):区分ECIF(行内客户信息)、CREDIT_BUREAU(征信)、HOUSING_FUND(公积金)等12种类型,每种类型对应不同的DataFetcher实现类。例如HousingFundFetcher会调用http://housing-api/v2/query?certNo={certNo},并自动处理429 Too Many Requests重试。清洗层(
IndicatorCleaner接口):每个指标可绑定一个清洗器。比如公积金月缴存额指标,其清洗器会过滤掉缴存状态 != 正常的记录,并对缴存金额字段做Math.abs()取绝对值——这是为防止上游系统传入负数导致后续计算异常。衍生层(
DerivativeIndicatorCalculator):支持四则运算的DSL解析。看这段真实代码(DerivativeIndicatorCalculator.java第63行):public BigDecimal calculate(Map<String, Object> context) { // 表达式如 "(base_ind_001 + base_ind_002) * 0.8 - base_ind_003" String expression = this.derivationExpression; // 使用AviatorEvaluator安全解析,避免脚本注入 Expression compiled = AviatorEvaluator.compile(expression); // 将context中指标名映射为变量名(base_ind_001 → ind001) Map<String, Object> env = new HashMap<>(); context.forEach((key, value) -> { String varName = key.replace("base_ind_", "ind"); env.put(varName, value instanceof Number ? value : BigDecimal.ZERO); }); return (BigDecimal) compiled.execute(env); }注意:
AviatorEvaluator比GroovyShell更轻量且沙箱更严,这是为防业务人员在衍生指标里写Runtime.getRuntime().exec("rm -rf /")留的后门。授权层(
IndicatorPermissionService):指标不是全局可见的。indicator_permission表存储product_code(产品编码)与indicator_code的多对多关系。当风控人员配置“小微易贷”产品的规则时,下拉框里只显示被授权给XIAO_WEI_YI_DAI的指标——这是通过@PreAuthorize("@indicatorPermissionService.hasPermission(#productCode, #indicatorCode)")AOP切面实现的。
3.2 规则引擎:操作规则与决策表的双轨执行模型
规则引擎不在com.data.decision.rule,而在com.data.decision.engine包。它不依赖IBM ODM或Drools,而是自研的轻量级执行器,核心是RuleExecutor接口的两个实现:
操作规则(OperationRule):对应
RuleType.OPERATION,结构最简单:{ "condition": "ind001 > 1000 && ind002 == 'ACTIVE'", "action": "setScore(85); setResult('APPROVED');" }执行时,
OperationRuleExecutor会用AviatorEvaluator解析condition,再用ScriptEngineManager(JavaScript)执行action字符串。注意:setScore()等方法是预注册的Java方法,不是JS原生函数。决策表(DecisionTable):对应
RuleType.DECISION_TABLE,用于评分卡。源码中DecisionTableExecutor.java的关键逻辑是矩阵匹配:public RuleExecutionResult execute(DecisionTable table, Map<String, Object> context) { // table.rows 是List<DecisionTableRow>,每行含 condition 和 action for (DecisionTableRow row : table.getRows()) { // condition 如 "age >= 25 && age <= 35 && income > 10000" if (AviatorEvaluator.execute(row.getCondition(), context)) { // 执行该行的action,如 "addScore(20); addReason('年龄适中');" return executeAction(row.getAction(), context); } } return defaultResult; // 未匹配时的兜底 }血泪经验:决策表的
condition字段必须是合取范式(CNF),即只能用&&连接原子条件,不能出现||。因为源码里没有实现析取范式解析器——这是为性能做的妥协,业务人员配置时需自觉拆分A || B为两条独立规则。
3.3 流程调度引擎:可视化编排背后的有向无环图(DAG)
调度不是简单串行调用,而是基于FlowNode的DAG执行。com.data.decision.scheduler包中,FlowScheduler是总控,它把decision_flow表中配置的流程,转为内存中的DirectedAcyclicGraph<FlowNode>。每个FlowNode有nodeType(如DATA_FETCH,RULE_EXECUTE,RESULT_PUBLISH)和nextNodes(出边列表)。
关键设计在于等待机制(Wait Mechanism):当某节点依赖多个上游(如RULE_EXECUTE需等ECIF_FETCH和CREDIT_BUREAU_FETCH都完成),FlowScheduler不会轮询,而是用CountDownLatch:
// FlowNodeExecutor.java 第142行 public void waitForDependencies(String nodeId, List<String> dependencies) { CountDownLatch latch = new CountDownLatch(dependencies.size()); dependencyLatchMap.put(nodeId, latch); // 存入ConcurrentHashMap // 每个上游节点完成时调用此方法 public void markDependencyComplete(String nodeId) { CountDownLatch latch = dependencyLatchMap.get(nodeId); if (latch != null) latch.countDown(); } // 当前节点阻塞等待 try { latch.await(30, TimeUnit.SECONDS); // 超时30秒抛异常 } catch (InterruptedException e) { throw new FlowExecutionException("Timeout waiting for dependencies"); } }这解释了为什么文档里强调“数据等待机制避免数据漏过风险”——它用同步原语保证了强一致性,代价是单个流程最长耗时=所有分支中最长路径的耗时,而非各分支耗时之和。
4. 避坑指南:那些让开发半夜爬起来改的5个致命细节
4.1 现象:Kafka消费者组持续rebalance,日志刷屏Revoked partitions
原因:application-dev.yml中kafka.consumer.session-timeout-ms默认是10000(10秒),但流立方指标拉取平均耗时12秒,导致消费者心跳超时被踢出组。
解决:将session-timeout-ms调至30000,并同步增大max-poll-interval-ms到45000。修改后重启服务。
4.2 现象:规则配置页面保存后,决策结果里reason字段为空字符串
原因:RuleExecutionResult实体类中reason字段是List<String>,但MyBatis的<resultMap>未配置collection映射,导致从rule_execution_log表查出的reason_text字段(逗号分隔字符串)未被split成List。
解决:在RuleExecutionLogMapper.xml的<resultMap id="RuleExecutionResultMap">里,添加:
<collection property="reason" ofType="java.lang.String"> <constructor> <arg column="reason_text" javaType="java.lang.String"/> </constructor> </collection>并在RuleExecutionResult构造器中增加public RuleExecutionResult(String reasonText),内部执行this.reason = Arrays.asList(reasonText.split(","));。
4.3 现象:导入Excel指标时,中文列名解析为乱码,如客户年龄变成客æ·å¹´é¾„
原因:IndicatorImportService.java第89行用new FileInputStream(file)读取,未指定字符集,Windows系统默认GBK,而Excel实际是UTF-8。
解决:改为new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()),再用WorkbookFactory.create(mappedBuffer, "UTF-8")。
4.4 现象:ABTest功能开启后,同一笔申请在1分钟内返回不同决策结果(APPROVED vs REJECTED)
原因:ABTest分流逻辑在ABTestRouter.java中,用Math.abs(customerId.hashCode()) % 100计算分组,但customerId是字符串,hashCode()在不同JVM实例上可能不同,导致集群中分流不一致。
解决:改用MurmurHash3.hashUnencodedChars(customerId),该哈希算法跨语言/跨平台结果一致,且已引入guava依赖。
4.5 现象:流立方推送的实时指标(如最近30天设备申请次数)在决策中始终为null
原因:StreamFusionClient.java第67行调用restTemplate.getForObject(url, IndicatorResponse.class),但IndicatorResponse类中deviceApplyCount字段的@JsonProperty注解写成了"device_apply_count",而流立方API实际返回的是"deviceApplyCount"(驼峰)。
解决:将@JsonProperty("device_apply_count")改为@JsonProperty("deviceApplyCount"),或在IndicatorResponse类上加@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class)。
5. 二次开发实战:给现有平台加一个“动态阈值”规则组件
5.1 需求背景:风控策略师要根据市场波动自动调整规则阈值
业务方提了个需求:当前“公积金月缴存额>8000元”是硬编码阈值,但季度末银行放款激增时,他们想临时把阈值提到12000元,而无需发布新版本。这需要一个动态阈值规则组件,让策略师在Web界面输入base_indicator_code(如HOUSING_FUND_MONTHLY)和adjustment_factor(如1.5),系统自动计算8000 * 1.5 = 12000作为新阈值。
5.2 改造步骤:从数据库到前端的全链路
第一步:扩指标表,加动态阈值字段
在indicator_definition表中新增两列:
ALTER TABLE indicator_definition ADD COLUMN dynamic_threshold_enabled BOOLEAN DEFAULT FALSE, ADD COLUMN adjustment_factor NUMERIC(5,2) DEFAULT 1.0;注意:
NUMERIC(5,2)支持最大999.99,足够覆盖10倍以内的调整。
第二步:改规则条件解析器,支持动态阈值语法
在OperationRuleExecutor.java的parseCondition()方法中,加入对dynamic_threshold()函数的识别:
// 原condition如 "housing_fund_monthly > dynamic_threshold('HOUSING_FUND_MONTHLY', 1.5)" private BigDecimal resolveDynamicThreshold(String indicatorCode, BigDecimal factor) { // 1. 从indicator_definition查出该指标的baseValue(如8000) IndicatorDefinition def = indicatorDefinitionService.findByCode(indicatorCode); if (def == null || !def.isDynamicThresholdEnabled()) { throw new IllegalArgumentException("Dynamic threshold not enabled for " + indicatorCode); } // 2. 计算动态值:baseValue * factor return def.getBaseValue().multiply(factor); } // 在Aviator执行环境里注册函数 AviatorEvaluator.addFunction(new AbstractFunction() { @Override public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) { String code = FunctionUtils.getStringValue(arg1, env); BigDecimal factor = FunctionUtils.getNumberValue(arg2, env).toBigDecimal(); return AviatorDecimal.valueOf(resolveDynamicThreshold(code, factor)); } });第三步:前端规则配置页,加动态阈值开关
在src/main/resources/static/js/rule-config.js中,找到规则条件编辑器的初始化代码,在initConditionEditor()函数末尾添加:
// 动态阈值开关 $('#dynamic-threshold-toggle').change(function() { if ($(this).is(':checked')) { $('#adjustment-factor-group').show(); // 自动填充当前指标的baseValue const baseVal = $('#base-value-display').text(); $('#adjustment-factor').val((parseFloat(baseVal) * 1.2).toFixed(2)); } else { $('#adjustment-factor-group').hide(); } });对应HTML片段(rule-config.html):
<div class="form-check"> <input class="form-check-input" type="checkbox" id="dynamic-threshold-toggle"> <label class="form-check-label" for="dynamic-threshold-toggle">启用动态阈值</label> </div> <div id="adjustment-factor-group" style="display:none;"> <label>调整系数(baseValue × 系数):</label> <input type="number" id="adjustment-factor" step="0.1" min="0.1" max="10.0" value="1.2"> </div>第四步:验证与灰度发布
- 启动服务,进入规则配置页,勾选“启用动态阈值”,输入系数
1.5,保存规则 - 用curl发起测试请求,观察
decision-result日志中是否出现dynamic_threshold resolved: HOUSING_FUND_MONTHLY * 1.5 = 12000.00 - 在
application-dev.yml中配置abtest.enabled=true,将新规则分配给5%流量,监控decision_result表中dynamic_threshold_used字段的统计比例
从那以后我每次加新规则组件,都强制走一遍“数据库变更→后端解析器→前端UI→ABTest灰度”的闭环。不是为了炫技,而是因为去年某次上线“征信查询次数”新规则时,忘了在
RuleExecutionLog实体里加dynamicThresholdUsed布尔字段,导致ABTest报告里全是NULL,排查了6小时才发现是ORM映射漏了——那晚的咖啡,至今想起来都苦。
希望帮到你。
本文还有配套的精品资源,点击获取