跳转到内容

数据模型:项目、用户、订单和内容怎么设计

搭建交付 · 经验分享 · 2-3 小时

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

未核验

数据模型:项目、用户、订单和内容怎么设计 配图
学习结果
为自己的产品画出实体关系,写出核心表的字段、状态和约束,并用一组刁钻的样例数据检查过一遍。
时长
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”填补未知的价格。删除记录之前,想想有没有别的数据引用它、法律上需不需要保留;涉及账务的记录,通常应该匿名化,而不是直接删除。

学习步骤

  1. 列出产品里的对象,用“谁创建、谁修改、何时删除、和谁有关系”判断哪些需要单独成表。
  2. 为每个实体确定 id、slug 和 title,并统一字段命名。
  3. 为有阶段变化的实体写出状态和允许的流转方向。
  4. 区分各类时间字段的含义,确认没有把更新时间当成核验时间。
  5. 为订单这类交易记录设计成交快照,为交易号加唯一约束。
  6. 准备一组刁钻样例数据,走一遍列表、详情、搜索和后台编辑。

场景拆解

一个目录站加上付费推荐位

小许的工具目录站准备加一个功能:工具开发者可以付费购买首页推荐位,每次一周。

  1. 新实体:推荐位购买需要单独成为“订单”实体,它有自己的状态(待支付、已支付、已退款)和生命周期,不能塞进工具记录的 metadata 里。
  2. 快照:订单保存购买时的价格、币种和推荐的起止日期。之后调价,不影响历史订单。
  3. 标记分离:工具表增加一个“是否为付费推荐”的字段,首页把推荐位和自然排序分开展示,并标明“推广”。
  4. 权限:开发者只能看到自己的订单;公开接口不返回任何订单和联系信息。
  5. 样例检查:用“推荐期已过但订单未退款”“同一工具连续购买两周”“退款后推荐位是否下线”三种情况检查逻辑。

检查中发现:推荐期结束后,首页没有自动撤下推荐标记。原因是“是否推荐”被存成了工具的一个固定字段,而不是由当前有效的订单决定。他改成根据订单的起止日期判断,问题就解决了。

补充材料

思维导图、练习、清单和参考资料可按需展开。

01

思维导图和离线文件

适合读完后下载到自己的知识库里继续整理。

设计前的五件事

  • 实体:值不值得单独成表
  • 标识:id、slug、title 分开
  • 状态:允许的流转
  • 关系:交易保存快照
  • 约束:在写入一侧校验

容易混淆的字段

  • 更新时间不等于核验时间
  • 金额用整数记最小单位
  • 交易号加唯一约束
  • 推广标记与自然排序分开

上线前检查

  • 用刁钻样例走一遍
  • 改结构时写清旧数据处理
  • 公开接口不含私密字段
  • 账务记录匿名化而不是删除

数据模型:项目、用户、订单和内容怎么设计 思维导图

02

实践练习

根据文章内容记录自己的判断与依据。

为你的产品设计核心表

选你产品里最核心的两到三个实体,完成一份数据设计。

准备材料

  • 产品的核心用户路径。
  • 纸笔或 dbdiagram、Excalidraw 这类画图工具。

练习步骤

01 / 找出实体和关系

时间: 30 分钟

操作:

  1. 列出对象,判断哪些需要单独成表。
  2. 画出实体之间的关系。

产出: 一张实体关系图。

检查: 每个实体都能回答“谁创建、谁修改、何时删除”。

02 / 写出字段、状态和约束

时间: 45 分钟

操作:

  1. 为每个实体写出字段、类型、是否必填、是否唯一。
  2. 写出状态和允许的流转,标出哪些字段不能公开。

产出: 一份字段表。

检查: 金额用整数记最小单位;交易记录有快照字段。

03 / 用样例数据检查

时间: 30 分钟

操作:

  1. 准备至少五条刁钻的样例数据。
  2. 在纸上或原型里走一遍列表、详情、搜索和后台编辑。

产出: 一份发现的问题清单。

检查: 每个问题都写了对应的修改。

完成检查

  • 标识、显示名和网址标识分开。
  • 状态流转和时间字段含义清楚。
  • 约束和权限在写入一侧执行。
03

清单、模板和关键词

用于项目记录和自查。

字段设计表

字段 类型 必填 唯一 是否公开 说明
id uuid 是 是 否 内部关联
slug text 是 是 是 网址标识
title text 是 否 是 显示名
status text 是 否 视情况 允许的值和流转
created_at 时间 是 否 否 创建时间

刁钻样例清单

重复的 slug、没有图片、超长标题、缺少来源、核验日期很旧、金额为 0、已退款的订单、被删除的关联对象。

关键概念

  • 实体:可以独立管理、有自己生命周期的对象
  • id、slug、title:内部标识、网址标识和显示名
  • 状态流转:对象允许从哪个状态变到哪个状态
  • 快照:交易发生时保存的事实
  • 唯一约束:防止重复记录
  • 行级安全:按用户和角色限制能读写哪些行

容易误判

  • 把所有信息塞进一个 metadata 字段
  • 用标题当网址,改标题后链接失效
  • 把更新时间当成核验时间
  • 用现在的商品价格计算历史订单
  • 只在浏览器里做校验
  • 直接删除账务记录,而不是匿名化
04

工具和参考资料

本篇涉及的工具和参考链接。

可参考工具

  • Supabase
  • Cloudflare D1
  • dbdiagram.io
  • Excalidraw

参考资料