先想清楚有哪些实体、怎么标识、有哪些状态、彼此什么关系,搜索、筛选、订单和权限才不会反复返工。

- 学习结果
- 为自己的产品画出实体关系,写出核心表的字段、状态和约束,并用一组刁钻的样例数据检查过一遍。
- 时长
- 2-3 小时
- 难度
- 经验分享
- 核验
- 未核验
正文
数据结构是产品里最难改的部分。页面样式可以随时调,按钮可以随时加;但表结构定下来、积累了真实数据之后,每改一个字段都要考虑旧数据怎么迁移、哪些页面会受影响。很多“返工”,根源是一开始没想清楚数据。
这篇不讲数据库理论,只讲动手设计前要想清楚的五件事:实体、标识、状态、关系、约束。
一、哪些东西值得单独成为一个实体
实体是可以独立管理的对象:项目、文章、用户、订单。判断一个东西要不要单独建表,问几个问题:它由谁创建、谁能修改、什么时候删除?它和其他东西有什么关系?它有没有自己的状态变化?
项目和订单的生命周期完全不同,不应该一股脑塞进一个“万能”的 metadata 字段里。确实变化多端、不需要筛选和查询的附加信息,可以用 JSON 字段存;但需要搜索、筛选、排序、统计的字段,一定要单独成列。
还没有需求的实体先别建。第一版不收费,就先不设计订单表。
二、标识和显示名分开
一个对象通常需要三种“名字”,各有用途:
- id:内部唯一标识,用于表之间的关联,永远不变。
- slug:出现在网址里的标识,比如
/projects/my-tool/,要稳定。 - title:给人看的名字,可以随时改。
改标题不应该影响订单关联,也不应该让详情页网址失效。如果 slug 不得不改,要给旧网址加上跳转,否则外部链接和搜索结果都会失效。
同一类实体,字段名要统一。一部分记录用 name,另一部分用 title,读取的代码里就会到处是判断分支。
三、状态和时间字段

状态字段描述一个对象处在哪个阶段,以及允许怎样流转。比如投稿:待审核(pending)可以变成已通过(approved)或已拒绝(rejected);公开列表只读取已通过的记录。后台编辑也要遵守同样的规则,不能绕开审核直接改成公开。
时间字段各有含义,不要混用:
| 字段 | 含义 |
|---|---|
created_at |
记录在系统里创建的时间 |
updated_at |
记录最后一次被修改的时间 |
published_at |
对外公开的时间 |
verified_at |
最后一次核对来源事实的时间 |
常见的错误是把“更新时间”当成“核验时间”:改了一个错别字,不代表资料重新核对过。来源有变化,就要重新核对,而不是只刷新日期。
四、关系和快照

订单关联用户和商品,但订单里要保存成交那一刻的快照:商品名、成交金额、币种。商品的价格以后会变,如果订单只记一个商品 ID,再用现在的价格去算历史订单,账就对不上了。这种“重复”不是冗余,它记录的是交易事实。
同样,分类和标签最好结构化保存,方便筛选。广告或合作推广的标记要单独成字段,和自然排序分开,不能混在一起。
下面是一个简化的例子(以 PostgreSQL 为例,省略了用户表):
create table products (
id uuid primary key,
slug text not null unique, -- 网址标识,唯一
title text not null, -- 显示名,可修改
price_cents integer not null check (price_cents >= 0),
currency text not null,
status text not null check (status in ('draft', 'published', 'archived')),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table orders (
id uuid primary key,
user_id uuid not null references users(id),
product_id uuid not null references products(id),
product_title text not null, -- 成交时的商品名快照
amount_cents integer not null, -- 实际成交金额,以“分”记
currency text not null,
status text not null check (status in ('pending', 'paid', 'refunded')),
provider_payment_id text unique, -- 支付平台的交易号,防止重复记账
created_at timestamptz not null default now()
);
注意金额用整数记“分”,不用浮点数;支付平台的交易号设为唯一,同一笔支付通知重复到达时,不会生成两条订单。
五、约束和权限放在写入的一侧
网址格式、金额范围、必填字段、唯一标识,都要在服务器端或数据库里校验。浏览器里的校验只是为了给用户及时的提示,可以被轻易绕过。
公开接口里不能出现联系方式和付款记录。使用托管数据库时,要按用户和角色设置读写权限,比如 Supabase 的行级安全策略(Row Level Security),确保一个用户只能读到自己的订单。
用刁钻的样例数据检查一遍
设计完之后,准备一组“刁钻”的样例:重复的 slug、没有图片、特别长的标题、缺少来源、核验日期很旧、金额为 0、已退款的订单。拿它们走一遍列表、详情、搜索和后台编辑,结构上的漏洞往往这时才暴露出来。
以后每次改表结构,都写清楚旧记录怎么处理。不要用默认值掩盖“其实不知道”的情况,比如用“0”填补未知的价格。删除记录之前,想想有没有别的数据引用它、法律上需不需要保留;涉及账务的记录,通常应该匿名化,而不是直接删除。
学习步骤
- 列出产品里的对象,用“谁创建、谁修改、何时删除、和谁有关系”判断哪些需要单独成表。
- 为每个实体确定 id、slug 和 title,并统一字段命名。
- 为有阶段变化的实体写出状态和允许的流转方向。
- 区分各类时间字段的含义,确认没有把更新时间当成核验时间。
- 为订单这类交易记录设计成交快照,为交易号加唯一约束。
- 准备一组刁钻样例数据,走一遍列表、详情、搜索和后台编辑。
场景拆解
一个目录站加上付费推荐位
小许的工具目录站准备加一个功能:工具开发者可以付费购买首页推荐位,每次一周。
- 新实体:推荐位购买需要单独成为“订单”实体,它有自己的状态(待支付、已支付、已退款)和生命周期,不能塞进工具记录的 metadata 里。
- 快照:订单保存购买时的价格、币种和推荐的起止日期。之后调价,不影响历史订单。
- 标记分离:工具表增加一个“是否为付费推荐”的字段,首页把推荐位和自然排序分开展示,并标明“推广”。
- 权限:开发者只能看到自己的订单;公开接口不返回任何订单和联系信息。
- 样例检查:用“推荐期已过但订单未退款”“同一工具连续购买两周”“退款后推荐位是否下线”三种情况检查逻辑。
检查中发现:推荐期结束后,首页没有自动撤下推荐标记。原因是“是否推荐”被存成了工具的一个固定字段,而不是由当前有效的订单决定。他改成根据订单的起止日期判断,问题就解决了。
补充材料
思维导图、练习、清单和参考资料可按需展开。
01 思维导图和离线文件
适合读完后下载到自己的知识库里继续整理。
设计前的五件事
- 实体:值不值得单独成表
- 标识:id、slug、title 分开
- 状态:允许的流转
- 关系:交易保存快照
- 约束:在写入一侧校验
容易混淆的字段
- 更新时间不等于核验时间
- 金额用整数记最小单位
- 交易号加唯一约束
- 推广标记与自然排序分开
上线前检查
- 用刁钻样例走一遍
- 改结构时写清旧数据处理
- 公开接口不含私密字段
- 账务记录匿名化而不是删除

02 实践练习
根据文章内容记录自己的判断与依据。
为你的产品设计核心表
选你产品里最核心的两到三个实体,完成一份数据设计。
准备材料
- 产品的核心用户路径。
- 纸笔或 dbdiagram、Excalidraw 这类画图工具。
练习步骤
01 / 找出实体和关系
时间: 30 分钟
操作:
- 列出对象,判断哪些需要单独成表。
- 画出实体之间的关系。
产出: 一张实体关系图。
检查: 每个实体都能回答“谁创建、谁修改、何时删除”。
02 / 写出字段、状态和约束
时间: 45 分钟
操作:
- 为每个实体写出字段、类型、是否必填、是否唯一。
- 写出状态和允许的流转,标出哪些字段不能公开。
产出: 一份字段表。
检查: 金额用整数记最小单位;交易记录有快照字段。
03 / 用样例数据检查
时间: 30 分钟
操作:
- 准备至少五条刁钻的样例数据。
- 在纸上或原型里走一遍列表、详情、搜索和后台编辑。
产出: 一份发现的问题清单。
检查: 每个问题都写了对应的修改。
完成检查
- 标识、显示名和网址标识分开。
- 状态流转和时间字段含义清楚。
- 约束和权限在写入一侧执行。
03 清单、模板和关键词
用于项目记录和自查。
字段设计表
| 字段 | 类型 | 必填 | 唯一 | 是否公开 | 说明 |
|---|---|---|---|---|---|
| id | uuid | 是 | 是 | 否 | 内部关联 |
| slug | text | 是 | 是 | 是 | 网址标识 |
| title | text | 是 | 否 | 是 | 显示名 |
| status | text | 是 | 否 | 视情况 | 允许的值和流转 |
| created_at | 时间 | 是 | 否 | 否 | 创建时间 |
刁钻样例清单
重复的 slug、没有图片、超长标题、缺少来源、核验日期很旧、金额为 0、已退款的订单、被删除的关联对象。
关键概念
- 实体:可以独立管理、有自己生命周期的对象
- id、slug、title:内部标识、网址标识和显示名
- 状态流转:对象允许从哪个状态变到哪个状态
- 快照:交易发生时保存的事实
- 唯一约束:防止重复记录
- 行级安全:按用户和角色限制能读写哪些行
容易误判
- 把所有信息塞进一个 metadata 字段
- 用标题当网址,改标题后链接失效
- 把更新时间当成核验时间
- 用现在的商品价格计算历史订单
- 只在浏览器里做校验
- 直接删除账务记录,而不是匿名化
04 工具和参考资料
本篇涉及的工具和参考链接。
可参考工具
- Supabase
- Cloudflare D1
- dbdiagram.io
- Excalidraw