3步搞定persons:从语法到项目落地的源码解析

📅 发布时间:2026/9/22 6:42:47
3步搞定persons:从语法到项目落地的源码解析
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 用雪花算法还是自增”、“软删除后外键怎么处理”,都欢迎提问。