3. 金融数据模型设计:账户、交易、客户实体定义,资金流向关系建模
好,咱们直接进入正题。金融数据建模,说白了就是回答三个问题:谁的钱?从哪来?到哪去? 这三个问题搞定了,反洗钱、风控、资金追踪就都有了根基。
我个人习惯,拿到业务需求后,第一件事不是写代码,而是画实体关系图。你想想看,账户、交易、客户,这三个实体怎么定义,关系怎么连,直接决定了后续所有查询的效率和准确性。
3.1 核心实体定义
我们先看三个最基础的节点类型。嗯,这里要注意,金融场景下,每个属性都可能成为风控的关键字段。
3.1.1 客户节点 (Customer)
客户是资金的最终所有者。在 Neo4j 里,我通常这样定义标签和属性:
// 客户节点标签:Customer
// 属性设计
{
customerId: "CUST_2024001", // 唯一客户编号
name: "张三", // 客户姓名
idType: "身份证", // 证件类型
idNumber: "110101199001011234", // 证件号码(需脱敏)
riskLevel: "high", // 风险等级:low/mid/high
createdDate: "2024-01-15", // 开户日期
status: "active" // 状态:active/frozen/closed
}
riskLevel 属性,我建议用枚举值而不是数字。为什么?因为查询时可读性更强,比如 WHERE c.riskLevel = 'high',一眼就能看懂。我曾经在一个项目里用了数字 1/2/3,结果半年后没人记得哪个数字对应哪个等级了。
3.1.2 账户节点 (Account)
账户是资金的载体。一个客户可以有多个账户,这是典型的 1:N 关系。
// 账户节点标签:Account
// 属性设计
{
accountId: "ACC_6222021234567890", // 账号
accountType: "savings", // 账户类型:savings/checking/credit
currency: "CNY", // 币种
balance: 150000.00, // 当前余额
openDate: "2023-06-01", // 开户日期
lastTransactionDate: "2024-03-10", // 最后交易日期
status: "normal" // 状态:normal/overdrawn/frozen
}
balance 设计成整数类型,结果遇到分币对账时,差了 0.01 元,排查了整整两天。金融场景下,金额字段一定要用 Decimal 或 BigDecimal,精度至少保留到小数点后 4 位。
3.1.3 交易节点 (Transaction)
交易是资金流动的原子事件。每个交易都连接两个账户,形成一条「资金流」。我建议把交易设计成关系,而不是节点。为什么?因为交易天然就是连接两个账户的边。
// 交易关系类型:TRANSFER
// 属性设计
{
transactionId: "TXN_20240310001", // 交易流水号
amount: 50000.00, // 交易金额
timestamp: "2024-03-10 14:30:00", // 交易时间
channel: "online", // 渠道:online/counter/ATM
purpose: "货款支付", // 交易用途
status: "completed" // 状态:pending/completed/failed
}
3.2 关系建模:资金流向
实体定义好了,接下来就是怎么连。金融数据模型的核心关系就两种:持有关系 和 转账关系。
3.2.1 持有关系 (OWNS)
客户和账户之间,用 OWNS 关系连接。方向从客户指向账户。
(:Customer)-[:OWNS {openedDate: "2023-06-01"}]->(:Account)
3.2.2 转账关系 (TRANSFER)
账户和账户之间,用 TRANSFER 关系连接。方向从付款方指向收款方。
(:Account)-[:TRANSFER {amount: 50000, timestamp: "2024-03-10 14:30:00"}]->(:Account)
3.3 完整数据模型图
下面这张图,是我用 SVG 手绘的模型结构。你看一眼,基本就能理解整个资金流向的脉络了。
3.4 创建模型的 Cypher 语句
模型设计好了,咱们直接上代码。我建议你按这个顺序执行:先创建约束,再插入数据。
// 第一步:创建唯一约束(防止重复数据)
CREATE CONSTRAINT unique_customer IF NOT EXISTS
FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;
CREATE CONSTRAINT unique_account IF NOT EXISTS
FOR (a:Account) REQUIRE a.accountId IS UNIQUE;
// 第二步:创建客户和账户
CREATE (c:Customer {
customerId: "CUST_001",
name: "张三",
riskLevel: "low",
status: "active"
});
CREATE (a1:Account {
accountId: "ACC_001",
accountType: "savings",
balance: 100000.00,
status: "normal"
});
CREATE (a2:Account {
accountId: "ACC_002",
accountType: "checking",
balance: 50000.00,
status: "normal"
});
// 第三步:建立持有关系
MATCH (c:Customer {customerId: "CUST_001"})
MATCH (a:Account {accountId: "ACC_001"})
CREATE (c)-[:OWNS {openedDate: "2024-01-01"}]->(a);
// 第四步:创建一笔转账交易
MATCH (from:Account {accountId: "ACC_001"})
MATCH (to:Account {accountId: "ACC_002"})
CREATE (from)-[:TRANSFER {
transactionId: "TXN_001",
amount: 30000.00,
timestamp: datetime("2024-03-10T14:30:00"),
channel: "online",
status: "completed"
}]->(to);
TRANSFER 关系走,不需要额外的 JOIN 操作。在反洗钱场景中,这种模型可以让你用一条 Cypher 语句就查出「张三的所有资金最终流向了哪些账户」,性能比关系型数据库快 10 倍以上。
3.5 模型设计中的常见陷阱
我踩过不少坑,挑两个典型的说说:
- 不要把交易设计成节点:我见过有人把交易做成独立节点,然后用
FROM和TO关系连接账户。结果查询「A到B的转账路径」时,需要遍历三层节点,性能直接崩了。记住,交易就是关系,不是节点。 - 别忘了时间索引:金融查询经常按时间范围筛选。如果你不建时间索引,查询「最近7天的可疑交易」会全表扫描。我建议对
timestamp属性建索引:CREATE INDEX transaction_time IF NOT EXISTS FOR ()-[r:TRANSFER]-() ON (r.timestamp)。
3.6 模型验证:一个简单的资金追踪查询
模型建得好不好,跑个查询就知道了。下面这个例子,查出张三账户所有转出的资金,最终流向了哪些账户:
// 查询:张三的资金流向(两层深度)
MATCH (c:Customer {name: "张三"})-[:OWNS]->(from:Account)
MATCH (from)-[t:TRANSFER]->(to:Account)
RETURN from.accountId AS 付款账户,
to.accountId AS 收款账户,
t.amount AS 金额,
t.timestamp AS 时间
ORDER BY t.timestamp DESC
LIMIT 10;
这个查询在关系型数据库里可能需要三张表 JOIN,但在图数据库里,就是一次路径遍历。你想想看,如果要做五层、十层的资金穿透,图数据库的优势就更明显了。
好了,这一章的内容就到这里。模型设计是地基,地基打牢了,后面的风控规则、资金追踪才能跑得稳。记住三个核心:客户是主体,账户是载体,交易是关系。
公众号:蓝海资料掘金营,微信deep3321