支持多种账户类型登录的 Database 设计
结论
不要为“Facebook 用户、密码用户、访客”分别建立三套用户表。更合适的设计是拆分下面几个概念:
customers:业务主体,表示实际购买服务的人。auth_identities:登录凭证,记录客户使用哪种方式认证。orders:订单或服务购买记录。payments:支付记录。- 订单联系人信息:保存支付时的信息快照,不能只依赖用户资料。
密码登录和 Facebook 登录只是两种认证方式,可以关联到同一个 customer。访客付款时,可以创建一条没有登录凭证的 customer 记录,也可以只在订单中保存访客信息。选择哪种方式,取决于业务是否需要识别回头客。
不要因为 email 相同,就自动合并 Facebook 身份和密码账户。email 可能未经验证,也可能发生变化或被多个身份错误复用。只有验证结果可信,或者用户主动确认时,才能关联账户。
推荐的数据模型
核心关系如下:
customers
1 ─── N auth_identities
1 ─── N orders
1 ─── N payments
一个客户可以没有登录方式,也可以同时拥有多种登录方式:
customer #123
├── password
└── facebook
只使用 Facebook 登录、从未设置密码的用户不需要特殊处理。系统创建一条 customers 记录,再创建一条 provider = 'facebook' 的认证记录即可,不必伪造密码。
SQL 表结构示例
下面使用 PostgreSQL 风格的 SQL。实际字段应根据业务和隐私合规要求调整。
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
display_name VARCHAR(200),
primary_email VARCHAR(320),
email_verified_at TIMESTAMPTZ,
status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
customers 保存业务层面的客户资料,不存放具体的登录凭证。
CREATE TABLE auth_identities (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL
REFERENCES customers(id),
provider VARCHAR(50) NOT NULL,
provider_subject VARCHAR(255) NOT NULL,
password_hash TEXT,
provider_email VARCHAR(320),
provider_email_verified BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMPTZ,
UNIQUE (provider, provider_subject)
);
各字段的用途如下:
provider:认证方式,例如password或facebook。provider_subject:用户在该认证系统中的稳定标识。- Facebook 登录应保存提供方返回的用户 ID,也就是 OAuth/OIDC 中类似
sub的稳定标识,不能把 email 当作外部账户主键。 password_hash:只有密码身份需要填写,Facebook 身份应为NULL。UNIQUE (provider, provider_subject):防止同一个外部身份绑定到多个客户。
如果密码登录只允许使用 email,可以在应用中把规范化后的 email 作为密码身份的登录名。密码哈希仍须使用 Argon2id、bcrypt 或 scrypt 等专用算法生成。不能保存明文密码,也不能直接用普通 SHA-256 代替密码哈希算法。
订单需要同时保存客户关联和购买时的联系人快照:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT
REFERENCES customers(id),
contact_name VARCHAR(200) NOT NULL,
contact_email VARCHAR(320) NOT NULL,
contact_phone VARCHAR(50),
amount_minor BIGINT NOT NULL,
currency CHAR(3) NOT NULL,
status VARCHAR(30) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
金额建议使用最小货币单位,例如分,不要使用浮点数:
1999 = 19.99 CNY
支付记录单独存放:
CREATE TABLE payments (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL
REFERENCES orders(id),
payment_provider VARCHAR(50) NOT NULL,
provider_payment_id VARCHAR(255),
amount_minor BIGINT NOT NULL,
currency CHAR(3) NOT NULL,
status VARCHAR(30) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (payment_provider, provider_payment_id)
);
订单和支付应当分开,因为同一笔订单可能经历多次支付尝试,也可能出现支付失败、退款或重新付款。
三种使用场景如何处理
普通账户注册
注册成功后:
- 创建
customers记录。 - 创建
auth_identities记录。 - 将
provider设为password。 - 保存登录标识和安全生成的
password_hash。
示意数据:
customers
id: 101
primary_email: alice@example.com
auth_identities
customer_id: 101
provider: password
provider_subject: alice@example.com
password_hash: <password hash>
provider_subject 的具体格式由系统决定。如果允许用户修改登录 email,最好设置一个不可变的内部登录标识。另一种做法是在修改 email 时,同时完成重新验证和唯一性检查。
仅使用 Facebook 登录
用户第一次通过 Facebook 登录时:
- 验证 Facebook 返回的授权结果。
- 读取 Facebook 提供的稳定账户 ID。
- 查询
(provider, provider_subject)是否已经存在。 - 如果不存在,创建
customers记录。 - 创建对应的
auth_identities记录,不设置密码。
customers
id: 102
primary_email: bob@example.com
auth_identities
customer_id: 102
provider: facebook
provider_subject: 987654321
password_hash: NULL
用户以后再次登录时,系统通过 facebook + 987654321 找到 customer_id = 102。
即使 Facebook 没有返回 email,这套结构也能正常工作。系统可以要求用户在付款前补充联系人 email,并将其保存到订单中。
不注册的访客付款
访客付款通常有两种处理方式。
第一种是创建没有认证身份的客户:
customers
id: 103
primary_email: guest@example.com
auth_identities
无记录
这样便于统一处理订单查询、客服请求和后续账户升级。访客以后注册时,可以在完成 email 验证后,为这个 customer 添加密码或 Facebook 身份。
第二种是不创建客户,只在订单中保存联系人:
orders
customer_id: NULL
contact_name: Guest User
contact_email: guest@example.com
如果业务只关心单次交易,这种方式更简单。如果以后需要订单历史、售后服务、重复购买分析或访客转注册用户,创建一条没有认证身份的 customer 记录通常更方便。
无论采用哪种方式,订单都应保留联系人快照。否则用户修改个人资料后,旧订单中的开票、通知或服务交付信息也会跟着变化。
如何关联 Facebook 与已有密码账户
不要因为两个账户的 email 相同就直接合并。更安全的流程是:
- 用户先登录已有的密码账户。
- 用户选择“绑定 Facebook”。
- 系统完成 Facebook 授权,取得稳定的 Facebook 用户 ID。
- 检查该 Facebook 身份是否已经绑定其他客户。
- 确认没有冲突后,为当前
customer_id添加一条 Facebook 身份记录。
结果如下:
customers
id: 101
auth_identities
customer_id: 101, provider: password, provider_subject: alice@example.com
customer_id: 101, provider: facebook, provider_subject: 123456789
如果用户先用 Facebook 登录,之后想设置密码,也不需要创建新的客户。验证 email 后,直接为当前 customer 增加一条 password 身份即可。
如果系统打算在用户首次通过 Facebook 登录时,自动关联已有的密码账户,至少应满足这些条件:
- Facebook 返回了 email;
- 提供方明确确认该 email 已验证;
- 本地账户的 email 也已验证;
- 系统已经处理账户接管和身份冲突风险。
即使满足这些条件,让用户登录原账户后主动绑定,通常还是更稳妥。
登录与建单逻辑示例
function loginWithFacebook(facebookToken):
profile = verifyFacebookToken(facebookToken)
identity = findIdentity(
provider = "facebook",
providerSubject = profile.userId
)
if identity exists:
return createSession(identity.customerId)
customer = createCustomer(
displayName = profile.name,
primaryEmail = profile.verifiedEmail
)
createIdentity(
customerId = customer.id,
provider = "facebook",
providerSubject = profile.userId,
providerEmail = profile.email,
providerEmailVerified = profile.emailVerified
)
return createSession(customer.id)
访客付款可以这样处理:
function createGuestOrder(input):
customer = createCustomer(
displayName = input.name,
primaryEmail = input.email
)
return createOrder(
customerId = customer.id,
contactName = input.name,
contactEmail = input.email,
amountMinor = input.amountMinor,
currency = input.currency
)
如果不想为每个访客创建客户,可以把 customerId 设为 NULL,但其余联系人信息仍要写入订单。
需要特别注意的问题
不要用 email 代替 Facebook 用户 ID
email 是联系方式,不是可靠的外部身份主键。它可能缺失或发生变化,也可能因为配置和授权范围不同而无法取得。识别外部账户时,应使用“认证提供方 + 提供方用户 ID”。
区分当前资料和交易快照
customers.primary_email 是客户当前使用的 email,orders.contact_email 是客户下单时使用的 email。两者可能相同,但用途不同,不能只保留其中一个。
明确定义合并规则
确认两个 customer 属于同一个人后,需要通过专门的流程完成合并,其中包括:
- 转移身份凭证;
- 迁移订单归属;
- 检查重复身份;
- 记录操作审计;
- 处理冲突信息;
- 使会话失效或要求用户重新认证。
不能只因为 email 相同就自动触发合并。
限制认证记录的有效组合
为了便于理解,上面的示例把密码字段放在 auth_identities 中。实际项目还可以继续拆分:
auth_identities
password_credentials
oauth_identities
拆分后,可以分别约束密码凭证和 OAuth 凭证。如果认证方式不多,也可以使用单表,再通过数据库约束或应用层逻辑检查字段组合。
支付信息不要自行保存
银行卡号、安全码等敏感支付数据应交给合规的支付服务商处理。数据库通常只保存支付服务商生成的交易 ID、支付状态、金额和必要的审计信息。具体要求取决于支付渠道、所在地区和适用法规。
客户是谁、客户如何登录、客户购买了什么,这是三个不同的问题。把它们分别建模为 customers、auth_identities 和 orders/payments 后,Facebook 登录、密码登录和访客付款就能共用一套清晰的数据模型。
备注:内容仅供参考。
