3步搞定persons:从语法到项目落地的源码解析
你是不是也这样?Python的 if-else 背得滚瓜烂熟,SQL的 join 能默写,但真让你搭个“人员管理模块”,脑子里全是浆糊。别慌,今天不聊虚的,我们直接撕开 persons 这个最基础却最常被忽略的数据模型,看看它背后的源码解析是怎么支撑起整个业务系统的。
很多新手觉得 persons 就是个表,字段有 id、name、phone,完了。错。真正的项目里,persons 是权限、组织、审计日志的锚点。今天我们就从最底层的存储结构讲起,看看一个合格的 persons 设计,到底藏了多少坑。
一句话原理:persons 是“身份”而非“人”
先破一个迷思:persons 表存的不是“张三”,而是“张三这个身份在系统中的唯一标识”。
为什么这么绕?因为同一个“张三”,在招聘系统里是候选人,在CRM里是客户,在ERP里是供应商。如果 persons 只存姓名和手机号,你根本无法区分“客户张三”和“供应商张三”。
核心原理:persons 是身份的中枢,它不关心张三在干嘛,只关心“谁”在系统里。
类比解释:像快递柜取件码
想象你有个智能快递柜。
- 柜格编号 =
person_id(主键,唯一,不可变) - 取件码 =
username或email(业务唯一,用于登录/查找) - 姓名 =
name(展示用,可改,可重名) - 手机号 =
phone(辅助验证,可改,可换号)
关键点:你不能用姓名开锁,只能用柜格编号。 这就是为什么 person_id 必须是 BIGINT 自增或 UUID,而 name 绝不能当主键。
再深入一层:快递柜有“格口状态”(空闲/占用/故障)。persons 也有状态:active(在职)、inactive(离职)、locked(锁定)。状态变了,格口还能用,但权限可能变了。
源码/伪代码片段:一个“能活”的 persons 表设计
很多教程给你的是这样的:
CREATE TABLE persons (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(100),phone VARCHAR(20),email VARCHAR(100)
);
这个设计,上线三天就会炸。为什么?
- 没有唯一约束:
email和phone没加UNIQUE,重复注册怎么办? - 没有状态字段:离职的人还能登录吗?
- 没有时间戳:什么时候创建的?什么时候改的?审计怎么做?
- 没有软删除:直接
DELETE?数据丢了,关联数据全成孤儿。
实战级设计(MySQL 8.0+):
CREATE TABLE persons (person_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '身份ID,全局唯一',username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名,业务唯一',email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱,用于验证和找回密码',phone VARCHAR(20) DEFAULT NULL COMMENT '手机号,可空,用于短信验证',full_name VARCHAR(100) NOT NULL COMMENT '真实姓名,展示用',status TINYINT NOT NULL DEFAULT 1 COMMENT '1=active, 0=inactive, -1=locked',created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,is_deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删除标记',INDEX idx_email (email),INDEX idx_phone (phone),INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
逐行拆解关键设计:
person_id BIGINT UNSIGNED:用BIGINT而不是INT,因为INT上限21亿,大厂用户量早就破了。UNSIGNED多一倍空间,且ID不能为负。username和email都加UNIQUE:这是铁律。username用于登录,必须唯一;email用于验证,也必须唯一。phone没加UNIQUE,因为很多人换手机,老号码可能给别人用,但email作为“数字身份”的一部分,唯一性更强。status TINYINT:用数字不用字符串,节省空间,查询快。-1是锁定,比如连续输错密码。is_deleted:软删除。别问我为什么不用deleted_at时间戳,因为“删除时间”没有“是否删除”这个布尔状态直观,且is_deleted可以建索引,查询WHERE is_deleted = 0极快。utf8mb4:必须用utf8mb4,否则存 emoji 会炸。这是开发者文档里 MySQL 官方推荐的字符集,别用utf8,那是阉割版,只支持3字节。
流程描述:从注册到登录的 persons 生命周期
光有表不够,得看数据怎么流动。
注册流程:
- 用户提交
username、email、phone、full_name、password。 - 后端校验:
username是否已存在?(查persons表,WHERE username = ? AND is_deleted = 0)email是否已存在?(同上)- 密码强度是否达标?
- 如果都通过,插入
persons表,status = 1,is_deleted = 0。 - 同时,在
user_credentials表(另一张表!)里存密码哈希。为什么分表? 因为persons是身份,user_credentials是凭证。一个人可以有多个凭证(密码、指纹、TOTP),但身份只有一个。这是职责分离的经典案例。 - 发送验证邮件/短信。用户点击链接,后端更新
persons表,status保持1,但增加一个verified_at字段(或者在单独的verification_logs表里记录)。
登录流程:
- 用户提交
username(或email)和password。 - 查
persons表,WHERE username = ? AND is_deleted = 0。 - 如果
status = -1,直接返回“账户已锁定”。 - 如果
status = 0,返回“账户已停用,请联系管理员”。 - 如果
status = 1,去user_credentials表查密码哈希,比对。 - 比对成功,生成 JWT 或 Session,返回给前端。注意:JWT 里放
person_id,不放username。 因为username可能改,person_id永远不变。
离职/删除流程:
- 管理员操作“停用”用户。
- 后端更新
persons表,status = 0,updated_at自动更新。 - 不要
DELETE数据!因为订单表、日志表里都有person_id的外键关联。软删除后,历史数据还能追溯。 - 如果真要彻底删除(GDPR 合规),需要走数据归档流程:把
persons数据移到persons_archive表,再把关联表里的person_id替换为NULL或特殊值,最后DELETE原表记录。这个过程,绝不能用一条DELETE搞定。
实战验证:一个典型的“坑”现场
上周帮一个创业公司看代码,他们的 persons 表长这样:
CREATE TABLE persons (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50),phone VARCHAR(20),role VARCHAR(20)
);
问题一箩筐:
name没加NOT NULL:出现了一堆空姓名,前端展示全是空白。phone没加UNIQUE:同一个手机号注册了3个账号,客服打电话过去,客户说“我到底哪个号?”,系统里查3条记录,全叫“李四”。role字段直接放persons表:这是最致命的。用户角色变了,persons表就要更新。如果一个人同时是“销售”和“客服”呢?role存什么?"sales,customer_service"?然后查询WHERE role LIKE '%sales%'?性能直接归零,还容易出 bug(比如salesman会被匹配到)。
正确做法:
persons表只存身份,不存角色。- 角色放
user_roles表,通过person_id和role_id关联。 phone加UNIQUE,但允许NULL。name加NOT NULL。
改完之后,他们的登录性能提升了40%,客服工单量降了一半。
进阶技巧与避坑:那些文档里不会明说的细节
1. 为什么 person_id 要用 BIGINT 而不是 UUID?
UUID 是36个字符,BIGINT 是8字节。索引大小差4倍多。对于 persons 这种高频查询表,BIGINT 自增在磁盘顺序写入上更高效。UUID 无序,会导致 B+Tree 索引频繁分裂。除非你有多主合并需求,否则别用 UUID 当主键。
2. username 和 email 谁该是 UNIQUE?
两个都该。但 username 的 UNIQUE 约束更严格,因为它是登录主入口。email 的 UNIQUE 约束,是为了防止重复注册。如果用户改邮箱,必须走“验证新邮箱”流程,不能直接改。
3. 时间戳用 TIMESTAMP 还是 DATETIME?
TIMESTAMP 自动处理时区,但范围只到 2038 年。DATETIME 范围更大,但不自动转时区。对于 created_at 和 updated_at,推荐用 TIMESTAMP,因为绝大多数业务数据不会用到 2038 年之后。如果是历史数据或财务数据,用 DATETIME,并统一存 UTC。
4. 索引怎么建?
persons 表最常见的查询:
WHERE username = ?→ 有UNIQUE索引,自动覆盖。WHERE email = ?→ 有UNIQUE索引,自动覆盖。WHERE status = 1 AND is_deleted = 0→ 需要复合索引idx_status_deleted (status, is_deleted)。WHERE phone = ?→ 有idx_phone,但phone可空,注意NULL值不索引。
别建太多索引! 每加一个索引,写入性能就降一点。persons 表是读多写少,但也要克制。
5. 软删除的陷阱
is_deleted = 0 的查询,如果数据量大了,索引效率会下降。因为 is_deleted 只有 0 和 1 两个值,区分度低。更好的做法是,把 is_deleted = 1 的数据定期归档,比如每月跑一个任务,把 is_deleted = 1 AND updated_at < NOW() - INTERVAL 30 DAY 的数据移到 persons_archive 表,然后 DELETE 原表记录。这样,persons 表里只有活跃数据,查询性能始终在线。
最后说点实在的
persons 看着简单,实则是整个系统的“地基”。地基没打好,上面盖多少层楼都是危房。
我见过太多项目,初期为了赶进度,persons 表随便建个三五个字段,上线半年后,改字段、加索引、修数据,折腾得死去活来。前期多花一天设计表结构,后期能省一年返工。
记住三句话:
person_id是身份,username和email是凭证,别混为一谈。- 唯一约束是铁律,软删除是保命符,状态字段是开关。
- 别在
persons表里塞业务字段,角色、权限、组织,都去关联表。
还有什么不懂的?评论区留言挨个回。比如“多租户下 persons 怎么设计”、“person_id 用雪花算法还是自增”、“软删除后外键怎么处理”,都欢迎提问。