两种订单类型如何共用 payment 和 card details 实体
结论
两种订单可以共用同一组 Payment 和支付方式实体,但支付记录不应关联到 ORDER_TYPE。
ORDER_TYPE 表示订单类别,比如“商品订单”或“重复处方订单”。付款针对的是某一笔具体订单,因此更合适的设计是:
- 建立统一的
Order主实体; - 使用
order_type_id区分订单类型; - 通过
order_id将Payment关联到具体订单; - 将银行卡信息建模为可复用的
PaymentMethod,供付款记录引用。
这种设计既避免了实体重复,也能明确记录每次付款对应的订单。
推荐的实体关系
可以采用下面的结构:
OrderType 1 ─── N Order 1 ─── N Payment N ─── 1 PaymentMethod
│
├── ProductOrderDetails
│
└── PrescriptionOrderDetails
各实体分别负责:
OrderType:定义订单类别;Order:保存所有订单共有的数据;ProductOrderDetails:保存商品订单特有的数据;PrescriptionOrderDetails:保存处方订单特有的数据;Payment:记录付款行为、金额和状态;PaymentMethod:保存支付服务商标记化后的支付方式信息。
OrderType 是分类数据,Order 才是实际的业务交易。付款必须关联交易实例,而不能只关联订单分类。
假设系统中有 100 笔商品订单,它们都属于同一个 OrderType。如果 Payment 直接关联 OrderType,数据库就无法确定某笔付款属于其中哪一笔订单。
表结构示例
以下以关系型数据库为例。由于 Order 在某些数据库中可能是保留字,示例使用 customer_order 作为表名。
CREATE TABLE order_type (
order_type_id INTEGER PRIMARY KEY,
type_code VARCHAR(30) NOT NULL UNIQUE,
type_name VARCHAR(100) NOT NULL
);
CREATE TABLE customer_order (
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
order_type_id INTEGER NOT NULL,
order_status VARCHAR(30) NOT NULL,
ordered_at TIMESTAMP NOT NULL,
total_amount DECIMAL(12, 2) NOT NULL,
currency CHAR(3) NOT NULL,
FOREIGN KEY (order_type_id)
REFERENCES order_type(order_type_id)
);
订单类型表可以存放固定的分类数据:
INSERT INTO order_type (order_type_id, type_code, type_name)
VALUES
(1, 'PRODUCT', '商品订单'),
(2, 'REPEAT_PRESCRIPTION', '重复处方订单');
两类订单的付款记录可以放在同一张表中:
CREATE TABLE payment (
payment_id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
payment_method_id BIGINT,
amount DECIMAL(12, 2) NOT NULL,
currency CHAR(3) NOT NULL,
payment_status VARCHAR(30) NOT NULL,
provider_reference VARCHAR(255),
paid_at TIMESTAMP NULL,
FOREIGN KEY (order_id)
REFERENCES customer_order(order_id)
);
这里更适合使用 Order 1:N Payment,而不是简单的 1:1。即使当前规定一张订单只能付款一次,以后也可能遇到:
- 付款失败后重新支付;
- 分次付款;
- 部分退款;
- 更换银行卡后再次支付;
- 一张订单产生授权、扣款和退款等多笔支付交易。
如果业务能够明确保证每张订单只有一条付款记录,可以给 payment.order_id 增加唯一约束:
ALTER TABLE payment
ADD CONSTRAINT uq_payment_order UNIQUE (order_id);
不过在实际的支付系统中,保留一对多关系通常更便于扩展。
处理两种订单的专有字段
如果两种订单的大部分字段相同,可以只使用一张 customer_order 表,再通过 order_type_id 区分类型。
如果处方订单需要保存处方编号、开方医生或配药状态,而商品订单需要记录配送方式等专有字段,可以增加一对一的扩展表:
CREATE TABLE product_order_details (
order_id BIGINT PRIMARY KEY,
delivery_method VARCHAR(50),
FOREIGN KEY (order_id)
REFERENCES customer_order(order_id)
);
CREATE TABLE prescription_order_details (
order_id BIGINT PRIMARY KEY,
prescription_id BIGINT NOT NULL,
dispensing_status VARCHAR(30) NOT NULL,
FOREIGN KEY (order_id)
REFERENCES customer_order(order_id)
);
通用数据和支付关系只需定义一次,各类订单的专有数据则存入对应的扩展表。
应用层或数据库约束还需要保证:
PRODUCT类型的订单只能有product_order_details;REPEAT_PRESCRIPTION类型的订单只能有prescription_order_details;- 同一订单不能同时出现在两个扩展表中。
普通外键通常无法完整表达这类跨表约束,因此可能需要使用触发器、应用层校验,或者采用支持相应约束能力的数据库设计。
银行卡信息如何设计
不建议使用普通的 card_details 表保存完整银行卡资料,也不要保存:
- 完整银行卡号;
- CVV 或 CVC;
- 未加密的敏感认证数据。
更安全的方式是让合规的支付服务商保存真实卡片信息,数据库只保留支付令牌以及用于展示的非敏感信息:
CREATE TABLE payment_method (
payment_method_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
provider VARCHAR(50) NOT NULL,
provider_token VARCHAR(255) NOT NULL UNIQUE,
method_type VARCHAR(30) NOT NULL,
card_brand VARCHAR(30),
card_last4 CHAR(4),
expiry_month SMALLINT,
expiry_year SMALLINT,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
然后为 payment 到 payment_method 的关系补充外键:
ALTER TABLE payment
ADD CONSTRAINT fk_payment_method
FOREIGN KEY (payment_method_id)
REFERENCES payment_method(payment_method_id);
采用这种模型后:
- 一个客户可以保存多个支付方式;
- 同一个支付方式可以用于多个订单;
- 每笔付款仍然准确关联到具体订单;
- 删除或停用支付方式不会导致历史付款记录丢失。
如果系统不支持保存银行卡,Payment 也可以只记录支付服务商返回的交易引用,无需建立持久化的 PaymentMethod。
还需要区分“支付方式”和“支付交易”
这两个概念不应放在同一个实体中:
PaymentMethod表示“用什么支付”,例如某张 Visa 卡对应的支付令牌;Payment表示“发生了哪次支付”,其中包括金额、状态和交易编号。
同一张卡可以支付多张订单,一张订单也可能产生多次支付尝试,所以二者通常不是一对一关系。
注意事项
金额应使用 DECIMAL 或数据库提供的精确数值类型,不要使用 FLOAT。还应同时保存 ISO 4217 货币代码,例如 CNY、GBP。
付款状态应根据实际业务需要设置,例如:
PENDING
AUTHORIZED
CAPTURED
FAILED
CANCELLED
REFUNDED
PARTIALLY_REFUNDED
如果需要完整审计授权、扣款和退款过程,可以增加 payment_transaction 表,不要反复覆盖 payment 中原有的数据。
设计时应明确各实体的职责:订单类型用于分类,订单承载交易,付款关联具体订单,支付方式由付款引用。这样,两种订单就可以共用同一套清晰且安全的支付模型。
备注:内容仅供参考。
