4、核心数据模型设计:账单、支付、退款、对账等核心实体的数据模型设计,包括状态机与字段约束
在BMS(Billing Management System,计费管理系统)中,数据模型是系统的骨架。账单、支付、退款、对账这四个核心实体,不仅承载着资金流转的关键信息,更是财务合规、审计追溯和系统稳定性的基石。一个设计不良的数据模型,往往会导致状态混乱、对账不平、资金差错甚至法律风险。本章将深入剖析这四个核心实体的数据模型设计要点,重点讲解状态机的严谨定义与字段约束的落地实践,帮助学员建立一套高可用、高一致性的数据建模思维。
4.1 账单实体:业务发生的源头
核心原理:账单(Bill)是BMS中一切资金动作的起点,它记录了用户因使用产品或服务而产生的应付费用。账单模型的设计核心在于“快照”与“可变”的平衡——账单生成后,其金额、周期等核心属性应尽量保持稳定,而状态则随着支付、退款等动作发生流转。账单通常包含账单号、用户ID、订单ID、账单周期、应付金额、已付金额、账单状态、生成时间、到期时间等字段。其中,bill_id作为全局唯一主键,建议使用雪花算法或UUID生成,避免使用自增ID以防止数据迁移和合并时的冲突。
判断方法或操作流程:在设计账单状态机时,需要明确状态的可达矩阵。一个典型的账单状态机包含:PENDING(待支付)、PAID(已支付)、OVERDUE(已逾期)、CANCELLED(已取消)、CLOSED(已关闭)。状态流转规则如下:
PENDING→PAID:用户完成全额支付。PENDING→OVERDUE:超过到期日且未支付,由定时任务驱动。PENDING/OVERDUE→CANCELLED:管理员手动取消或系统自动取消(如订单关闭)。PAID→CLOSED:账单完成结算,进入归档状态。
禁止的状态跳转包括:PAID 回退到 PENDING,CANCELLED 重新激活等。这些规则必须在服务层通过状态机引擎强制校验,而非仅依赖数据库约束。
paid_amount字段更新,但status仍为PENDING,直到paid_amount >= amount时才触发状态迁移至PAID。这种设计避免了为“部分支付”单独创建状态,简化了状态机复杂度。
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| bill_id | BIGINT | PK, 非空 | 全局唯一账单号 |
| user_id | BIGINT | 索引, 非空 | 所属用户 |
| order_id | VARCHAR(64) | 可空 | 关联订单号 |
| amount | DECIMAL(12,2) | 非空, > 0 | 账单总额 |
| paid_amount | DECIMAL(12,2) | 默认 0 | 已支付金额 |
| status | VARCHAR(20) | 非空 | 状态机字段 |
| version | INT | 默认 0 | 乐观锁版本号 |
amount与paid_amount之间应通过数据库CHECK约束保证paid_amount <= amount。
4.2 支付实体:资金流动的通道
核心原理:支付(Payment)实体记录了用户为清偿账单而发起的每一笔资金操作。支付模型的设计核心在于“幂等性”与“可追溯性”。由于网络超时、重试等原因,同一笔支付请求可能被提交多次,因此必须通过biz_id(业务幂等键)保证唯一。支付状态机通常包含:INIT(初始化)、PROCESSING(处理中)、SUCCESS(成功)、FAILED(失败)、CLOSED(关闭)。其中,PROCESSING是中间态,系统需要配合支付渠道的回调或主动查询来推进状态。
判断方法或操作流程:支付状态流转的关键在于“最终一致性”。当用户发起支付时,系统创建INIT状态的支付单,并调用第三方支付渠道。渠道返回受理成功后,状态置为PROCESSING。随后,系统通过异步通知或定时轮询获取支付结果:
- 若渠道明确返回成功,则更新为
SUCCESS,并触发账单的PAID动作。 - 若渠道明确返回失败或超时关闭,则更新为
FAILED,允许用户重新发起支付。 - 若长时间处于
PROCESSING(如超过30分钟),需启动对账任务与渠道核实,避免资金悬挂。
支付单必须记录渠道流水号(channel_txn_no)、支付渠道、支付金额、支付时间等关键字段,用于后续对账与审计。
-- 支付表核心DDL示例
CREATE TABLE payment (
payment_id BIGINT PRIMARY KEY,
bill_id BIGINT NOT NULL,
biz_id VARCHAR(64) NOT NULL UNIQUE, -- 幂等键
channel VARCHAR(32) NOT NULL,
channel_txn_no VARCHAR(64),
amount DECIMAL(12,2) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
version INT DEFAULT 0,
CONSTRAINT fk_payment_bill FOREIGN KEY (bill_id) REFERENCES bill(bill_id),
CONSTRAINT chk_payment_amount CHECK (amount > 0)
);
SUCCESS时使用UPDATE payment SET status='SUCCESS', version=version+1 WHERE payment_id=? AND status IN ('INIT','PROCESSING'),通过受影响行数判断是否重复处理。
4.3 退款实体:逆向资金流的控制
核心原理:退款(Refund)是支付的逆操作,其数据模型设计比支付更为严格,因为涉及资金安全与风控。退款单必须关联原始的支付单,且退款总额不能超过支付净额(支付金额减去已退款金额)。退款状态机包含:REQUESTED(已申请)、APPROVED(已审批)、PROCESSING(处理中)、SUCCESS(成功)、REJECTED(已拒绝)。引入APPROVED中间态是为了满足财务人工审核或风控自动审核的场景。
判断方法或操作流程:退款流程通常由用户发起或系统触发(如订单取消)。操作流程如下:
- 创建退款单,状态为
REQUESTED,记录退款原因、退款金额。 - 系统校验退款金额合法性(累计退款 ≤ 支付金额 - 已退款金额)。
- 风控审核通过后,状态置为
APPROVED,调用支付渠道退款接口。 - 渠道返回受理后,状态置为
PROCESSING,等待渠道最终结果。 - 退款成功则置为
SUCCESS,并更新原支付单的已退款金额;失败则置为REJECTED,允许人工介入。
退款单需要额外记录refund_reason、operator_id(操作人)、approval_note(审批备注)等字段,以满足合规审计要求。
REQUESTED。风控规则引擎检测到该用户历史退款率异常,自动将状态置为REJECTED,并通知客服人工审核。客服核实后手动将状态改为APPROVED并执行退款。此案例展示了状态机中人工干预节点的必要性,以及REJECTED状态并非终态,可通过人工操作重新进入流程。
| 状态 | 允许操作 | 目标状态 | 触发条件 |
|---|---|---|---|
| REQUESTED | 风控审核 | APPROVED / REJECTED | 自动规则或人工 |
| APPROVED | 调用渠道 | PROCESSING | 退款接口受理 |
| PROCESSING | 渠道回调 | SUCCESS / REJECTED | 渠道明确结果 |
| REJECTED | 人工重试 | APPROVED | 管理员操作 |
4.4 对账实体:系统自愈的保障
核心原理:对账(Reconciliation)是BMS中保障资金一致性的最后一道防线。对账实体记录系统账单与支付渠道流水之间的核对结果。对账模型的核心在于“批次”与“差异处理”。对账批次通常按日期和渠道维度生成,包含对账日期、渠道、系统总金额、渠道总金额、差异金额、对账状态等字段。对账状态机相对简单:IN_PROGRESS(对账中)、MATCHED(一致)、MISMATCH(不一致)、ADJUSTED(已调整)。
判断方法或操作流程:对账流程通常由定时任务触发,核心步骤如下:
- 拉取渠道对账单文件,解析为渠道流水列表。
- 从本地数据库查询同一时间范围内的支付与退款流水。
- 以
channel_txn_no为关联键进行匹配,找出差异记录(本地有渠道无、渠道有本地无、金额不一致)。 - 生成差异报告,状态置为
MISMATCH。 - 财务人员根据差异报告进行调账处理(如补单、冲正),完成后状态置为
ADJUSTED。
对账表需要存储原始对账文件的MD5值,防止重复导入。同时,差异记录需要关联到具体的支付单或退款单,便于追踪。
SUCCESS,若渠道最终未结算,应启动自动冲正流程。
NOT NULL、CHECK、UNIQUE和FOREIGN KEY四种约束。此外,所有金额字段使用DECIMAL(12,2),所有时间字段使用DATETIME(3)并统一存储UTC时间,展示层再转换为本地时区。通过严谨的状态机定义和字段约束,BMS才能在高并发、高资金流动的场景下保持数据的一致性与可审计性。