PAYATHON 2026

如何设计支持多种 billing option 的 database schema?

支付小周

明确结论

可以分别为 Credit Card、House Account 和 COD 建立明细表。不过,应该先建立一张统一的账单主表,再让各类型明细表使用主表主键作为自己的主键和外键。

不建议在 ORDERS 中增加一个含义随 billingoption_id 变化的 billing_data_id。这种“多态外键”无法通过普通 foreign key 约束它究竟引用哪张表,很容易产生无效引用。

推荐使用以下关系:

ORDERS
  └── ORDER_BILLING
        ├── CREDIT_CARD_BILLING
        ├── HOUSE_ACCOUNT_BILLING
        └── COD_BILLING

如果每个订单只有一种结算方式,可以为 ORDER_BILLING.order_id 添加唯一约束。如果一个订单可能拆分付款或多次扣款,则应改为一对多关系。

为什么要分成主表和明细表

三种 billing option 有一些共同属性:

  • 所属订单
  • 应收金额
  • 币种
  • 支付状态
  • 创建时间
  • 完成时间

它们也各有不同的数据:

  • Credit Card:支付平台、token、卡号末四位
  • House Account:内部账户、账期或授权信息
  • COD:代收金额、物流单号、签收状态

如果把所有字段都放进一张表,会产生大量可空列,也很难约束每种类型只能填写自己的字段。如果完全拆成三张互不相关的表,查询、状态管理和外键设计又会变得复杂。

采用“统一主表 + 类型明细表”,可以同时处理公共字段、类型差异和引用完整性。

推荐的表结构

以下示例使用 PostgreSQL 风格的 SQL。其他数据库可能需要调整 identity、时间类型和约束语法。

CREATE TABLE billing_options (
    billing_option_code VARCHAR(30) PRIMARY KEY
);

INSERT INTO billing_options (billing_option_code)
VALUES
    ('CREDIT_CARD'),
    ('HOUSE_ACCOUNT'),
    ('COD');

CREATE TABLE orders (
    order_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

账单主表保存所有结算方式共有的数据:

CREATE TABLE order_billing (
    billing_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    order_id BIGINT NOT NULL,
    billing_option_code VARCHAR(30) NOT NULL,
    amount NUMERIC(12, 2) NOT NULL CHECK (amount >= 0),
    currency CHAR(3) NOT NULL,
    billing_status VARCHAR(30) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMP NULL,

    CONSTRAINT fk_order_billing_order
        FOREIGN KEY (order_id)
        REFERENCES orders (order_id),

    CONSTRAINT fk_order_billing_option
        FOREIGN KEY (billing_option_code)
        REFERENCES billing_options (billing_option_code),

    CONSTRAINT uq_order_billing_type
        UNIQUE (billing_id, billing_option_code),

    CONSTRAINT uq_one_billing_per_order
        UNIQUE (order_id)
);

uq_one_billing_per_order 表示一个订单只能有一条账单记录。如果业务允许拆单支付、组合支付或多次结算,应删除这个约束。

Credit Card 明细

CREATE TABLE credit_card_billing (
    billing_id BIGINT PRIMARY KEY,
    billing_option_code VARCHAR(30)
        NOT NULL DEFAULT 'CREDIT_CARD'
        CHECK (billing_option_code = 'CREDIT_CARD'),

    payment_provider VARCHAR(50) NOT NULL,
    payment_token VARCHAR(255) NOT NULL,
    card_brand VARCHAR(30),
    card_last_four CHAR(4),
    provider_transaction_id VARCHAR(100),

    CONSTRAINT fk_credit_card_billing
        FOREIGN KEY (billing_id, billing_option_code)
        REFERENCES order_billing (billing_id, billing_option_code)
);

House Account 明细

建议把 house account 建成独立实体,不要直接把账号文本写入订单。

CREATE TABLE house_accounts (
    house_account_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    account_number VARCHAR(50) NOT NULL UNIQUE,
    account_status VARCHAR(30) NOT NULL,
    credit_limit NUMERIC(12, 2)
);

CREATE TABLE house_account_billing (
    billing_id BIGINT PRIMARY KEY,
    billing_option_code VARCHAR(30)
        NOT NULL DEFAULT 'HOUSE_ACCOUNT'
        CHECK (billing_option_code = 'HOUSE_ACCOUNT'),

    house_account_id BIGINT NOT NULL,
    authorization_reference VARCHAR(100),

    CONSTRAINT fk_house_account_billing
        FOREIGN KEY (billing_id, billing_option_code)
        REFERENCES order_billing (billing_id, billing_option_code),

    CONSTRAINT fk_house_account
        FOREIGN KEY (house_account_id)
        REFERENCES house_accounts (house_account_id)
);

COD 明细

如果 COD 没有任何专属数据,可以不建明细表,只在 order_billing 中记录类型。如果需要记录代收和物流信息,则可以建立:

CREATE TABLE cod_billing (
    billing_id BIGINT PRIMARY KEY,
    billing_option_code VARCHAR(30)
        NOT NULL DEFAULT 'COD'
        CHECK (billing_option_code = 'COD'),

    collection_amount NUMERIC(12, 2)
        NOT NULL CHECK (collection_amount >= 0),
    carrier_name VARCHAR(100),
    tracking_number VARCHAR(100),
    collected_at TIMESTAMP NULL,

    CONSTRAINT fk_cod_billing
        FOREIGN KEY (billing_id, billing_option_code)
        REFERENCES order_billing (billing_id, billing_option_code)
);

这里使用复合 foreign key,可以防止把 CREDIT_CARD 类型的账单错误写入 cod_billing。

创建账单记录的方式

以信用卡结算为例,主表和明细表应在同一个事务中写入:

BEGIN;

INSERT INTO order_billing (
    order_id,
    billing_option_code,
    amount,
    currency,
    billing_status
)
VALUES (
    10001,
    'CREDIT_CARD',
    299.00,
    'CNY',
    'PENDING'
)
RETURNING billing_id;

-- 假设返回 billing_id = 501
INSERT INTO credit_card_billing (
    billing_id,
    payment_provider,
    payment_token,
    card_brand,
    card_last_four
)
VALUES (
    501,
    'example_provider',
    'provider_generated_token',
    'VISA',
    '1234'
);

COMMIT;

事务可以避免出现只写入主表、没有写入对应明细表的半成品数据。

是否还要在 ORDERS 中保留 billingoption_id

通常不需要。结算方式已经记录在 order_billing.billing_option_code 中,如果再存入 orders,就会产生重复数据,两个字段也可能不一致。

如果经常需要从订单查询结算方式,可以使用关联查询:

SELECT
    o.order_id,
    b.billing_option_code,
    b.amount,
    b.currency,
    b.billing_status
FROM orders AS o
JOIN order_billing AS b
    ON b.order_id = o.order_id
WHERE o.order_id = 10001;

只有在明确接受数据冗余,并且能够通过数据库约束或统一写入流程保证同步时,才应考虑在 orders 中重复保存该字段。

需要注意的问题

不要保存完整信用卡数据

一般只应保存支付服务商提供的 token、卡品牌和末四位等信息,不要直接保存 CVV。是否允许保存完整卡号,以及如何加密、审计和限制访问,取决于适用的 PCI DSS 范围和支付服务商方案。

账单方式和支付交易不是同一个概念

一次信用卡结算可能经历授权、扣款、失败、重试和退款。不要通过不断覆盖 order_billing 来记录这些过程,最好另建交易流水表:

CREATE TABLE billing_transactions (
    transaction_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    billing_id BIGINT NOT NULL,
    transaction_type VARCHAR(30) NOT NULL,
    transaction_status VARCHAR(30) NOT NULL,
    amount NUMERIC(12, 2) NOT NULL,
    provider_reference VARCHAR(100),
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_billing_transaction
        FOREIGN KEY (billing_id)
        REFERENCES order_billing (billing_id)
);

“恰好存在一条对应明细”需要额外保证

普通 foreign key 可以防止写入错误类型的明细记录,但无法单独保证每条 order_billing 必然存在且只存在一种明细。这个条件可以通过事务和应用层校验来保证。如果所有写入入口都由数据库控制,也可以使用 deferred constraint trigger 进行严格校验。

是否需要为 COD 建表取决于实际字段

不要为了结构统一而强行建立空表。只有当 COD 确实包含代收金额、承运商、物流单号或收款时间等专属数据时,cod_billing 才有实际价值。

总体而言,ORDERS → ORDER_BILLING → 各类型明细表 是较为稳妥的设计。它既能避免多态 foreign key,也方便以后增加新的结算方式、支付状态和交易流水。

备注:内容仅供参考。