# 设计规范(PHP + MySQL)
> 通用设计规范:适用于基于 PHP(现代框架,如 Hyperf / Laravel / Symfony)与 MySQL 的后端服务。
> 规范级别约定:**[必须]** 强制要求,代码评审不通过项;**[应该]** 推荐做法,默认遵循;**[建议]** 可选优化。
> 提交评审前,代码必须通过本规范全部 **[必须]** 项与已适用的 **[应该]** 项。
## 适用范围
- 本规范适用于以下场景,必须触发自检:
- 新建表、修改表结构、设计库表时
- 编写或优化 SQL(查询 / 插入 / 更新 / 分页)时
- 生成 migration 脚本时
- 不适用于 PostgreSQL / MongoDB / Redis 等非 MySQL 场景
---
## 1. PHP 规范
### 1.1 语言与环境
- **[必须]** 使用受支持的 PHP 版本(≥ 8.1),每个文件首行声明 `declare(strict_types=1);`
- **[必须]** 使用 Composer 管理依赖并提交 `composer.lock`,环境构建基于锁文件,保证可复现
- **[必须]** 全项目统一时区(`date.timezone`),避免时区错位导致的时间类缺陷
- **[必须]** 使用框架能力(路由、ORM、容器、中间件、事件、队列)解决问题,不重复造轮子
- **[应该]** 业务代码中不使用全局变量、`$_GET/$_POST` 直接取值,统一经框架 Request 抽象
- **[应该]** 代码静态检查(phpstan / psalm 等)纳入 CI
### 1.2 编码风格与命名
- **[必须]** 遵循 PSR-12 编码风格
- **[必须]** 命名规则:
- 类 / 接口 / Trait:`PascalCase`;接口后缀 `Interface`,抽象类前缀 `Abstract`
- 方法 / 函数 / 变量 / 属性:`camelCase`
- 常量(类常量 / 全局常量):`SCREAMING_SNAKE_CASE`
- 布尔变量:`is` / `has` / `can` 语义前缀(如 `isLocked` / `hasToken` / `canDelete`)
- 私有/受保护成员:无下划线前缀,用可见性关键字表达
- **[必须]** 类文件遵循 PSR-4 自动加载;一个文件只声明一个类
- **[必须]** 方法必须有参数类型与返回类型声明;可空类型用 `?type`;联合类型用 `type1|type2`
- **[必须]** 常量集中定义(业务阈值、TTL、状态值),禁止散落魔法数字
- **[应该]** 注释只解释「为什么」,不赘述「做了什么」;逻辑复杂处必须写注释说明意图
- **[必须]** 注释 / 文档描述必须与代码实际行为一致(如声称「加密存储」就必须真的加密)
### 1.3 分层架构
- **[必须]** 三层职责分离:`Controller → Service → Model`,依赖方向单向、不得跨层
| 层 | 职责 | 禁止 |
|---|---|---|
| Controller | 参数接收、格式校验、调用 Service、统一响应 | 业务逻辑、SQL |
| Service | 业务规则、流程编排、事务边界、审计 | SQL 细节、请求/响应对象污染 |
| Model | 表映射、关系、查询作用域、类型转换 | 业务规则 |
- **[必须]** Controller 内禁止出现 SQL、循环、复杂条件分支;只做「取参 → 校验 → 调用 → 响应」
- **[必须]** 一个方法只做一件事,方法体超过约 60 行应拆分
- **[应该]** 相同实体聚合在同一 Service,禁止散落多处修改同一表
### 1.4 依赖注入
- **[必须]** 依赖通过构造函数注入,禁止在方法内部 `new` 核心服务、禁止静态方法承载业务逻辑
- **[必须]** 禁止在业务代码中直接使用服务容器全局访问(服务定位器反模式)
- **[应该]** 面向接口编程:核心依赖定义接口,实现可替换(便于 Mock 与测试)
### 1.5 输入校验
- **[必须]** 先校验、后处理:所有外部输入(请求体、查询参数、请求头、上传文件)先过校验再进入业务
- **[必须]** 使用白名单校验(格式、长度、枚举),校验失败返回语义化错误,不进入业务逻辑
- **[必须]** 对所有字符串输入做 `trim` 与类型强转,保证后续比较的类型一致
- **[必须]** 枚举类输入(如验证码用途、登录方式)必须限定合法取值集合
- **[应该]** 复杂校验抽为校验器/表单请求对象,避免散落在 Controller 各处
### 1.6 异常与错误处理
- **[必须]** 业务异常统一继承自定义异常基类,携带错误码与用户可读消息;禁止直接抛裸 `\Exception`
- **[必须]** HTTP 状态码语义化:
| 状态码 | 语义 |
|---|---|
| 400 | 参数错误 / 校验失败 |
| 401 | 未认证 / 凭证无效 |
| 403 | 无权限(含演示功能开关未开启) |
| 404 | 资源不存在 |
| 409 | 冲突(唯一性、重复操作) |
| 419 | CSRF 校验失败 |
| 422 | 语义 / 业务规则不满足 |
| 423 | 资源被锁定(账号锁定) |
| 429 | 限流 / 频控 |
- **[必须]** 全局异常处理器兜底:5xx 响应禁止返回堆栈、SQL、框架内部信息;异常详情只进服务端日志
- **[必须]** 未匹配路由返回 JSON 404,不返回框架默认 HTML 页面
- **[必须]** 禁止 `die` / `exit` / 裸 `@` 抑制错误;禁止空 `catch` 吞异常(必须记录日志或重新抛出)
- **[必须]** 日志分级正确(debug/info/warn/error),禁止把密码、令牌、验证码等敏感信息写入日志
### 1.7 安全编码
- **[必须]** 数据库访问一律走 ORM / 参数绑定,禁止字符串拼接 SQL
- **[必须]** 密码使用 bcrypt(cost ≥ 12)或 argon2 哈希,禁止明文、MD5、SHA1
- **[必须]** 验证码、令牌等一次性凭证:服务端生成 `random_bytes`/`random_int`,只存哈希(加盐),比较用 `hash_equals`(防时序攻击),设有效期、次数上限、一次性使用
- **[必须]** 登录失败返回统一文案,不泄露账号是否存在(防用户枚举)
- **[必须]** 写操作必须防 CSRF(双提交令牌或同源校验),比较用常数时间函数
- **[必须]** 认证令牌:短时效 Access + 可吊销、轮换的 Refresh;令牌存 httpOnly Cookie(SameSite),响应与日志中不得出现明文令牌
- **[必须]** 设备/会话维度支持吊销,吊销后相关令牌立即失效
- **[必须]** 上传文件:扩展名 + MIME 白名单、大小上限、内容魔数校验、服务端生成随机文件名、存储目录不可执行脚本
- **[必须]** 密码、私钥、盐、第三方凭证必须来自环境变量/配置中心,禁止硬编码;禁止使用默认密钥上线
- **[必须]** 敏感个人信息(证件号、银行卡号,手机号等)明文落库,接口只返回脱敏值
- **[必须]** 手机号全库统一「区号 + 号码」两段式:`country_code VARCHAR(8)`(E.164 格式,如 `+86`)+ `phone VARCHAR(20)`(号码本体,不含 `+`),禁止把区号拼进号码列
- **[应该]** 登录等敏感路径全量写审计日志(成功/失败、原因、IP、UA、设备),审计记录只追加、不修改不删除
### 1.8 并发与事务
- **[必须]** 计数累加 / 状态流转等读改写操作必须原子化:优先 SQL 原子更新(`SET x = x + 1`)或带条件更新,禁止纯「读-改-写」三段式(并发丢失更新)
- **[必须]** 业务唯一性由数据库唯一约束兜底,代码只做友好提示,禁止仅靠「先查后插」保证唯一
- **[必须]** 一次性资源(验证码、二维码票据、兑换码)的「领取/作废」必须原子(条件更新或加锁),防止并发重复消费
- **[必须]** 事务保持短小:事务内禁止远程调用(HTTP、短信、邮件)与长查询;事务边界清晰,只包裹需要一致性的写操作
- **[应该]** 高频读写热点按场景选择乐观锁(版本号)或悲观锁(行锁 `FOR UPDATE`),避免表锁
- **[应该]** 对外写接口设计幂等键,重复请求不产生重复副作用
### 1.9 性能
- **[必须]** 查询避免 N+1:关联数据使用预加载
- **[必须]** 列表/分页接口必须有数量上限(`per_page` 上下限兜底),禁止无界查询
- **[必须]** 禁止循环内查询数据库 / 循环内发远程请求
- **[必须]** 大量数据写入使用批量插入,避免逐条 insert
- **[应该]** 高频只读数据使用缓存,并明确缓存失效与一致性策略
### 1.10 配置与环境
- **[必须]** 配置分层(dev / test / prod),敏感配置不入版本库(提供 `.env.example` 模板)
- **[必须]** 生产环境必须关闭调试开关(DEBUG、验证码明文回显、演示查询接口等),且此类开关要有运行时门禁(开启才可用,关闭必须 403/404)
- **[必须]** 配置值使用前做类型强转,避免字符串/布尔歧义
### 1.11 测试
- **[必须]** 认证、支付、权限等关键流程必须有自动化测试(单元 / 集成 / e2e)
- **[应该]** 测试覆盖:正常路径、边界条件(空值、超长、越界)、异常路径、并发竞态
- **[应该]** 修复 bug 时先补回归测试
---
## 2. MySQL 规范
### 2.1 存储引擎与字符集
- **[必须]** 统一 InnoDB(事务 + 行锁 + 崩溃恢复)
- **[必须]** 统一字符集 `utf8mb4`;MySQL 8 采用排序规则 `utf8mb4_0900_ai_ci`(MySQL 5.7 采用 `utf8mb4_unicode_520_ci`)
- **[必须]** 建表语句必须带 `ENGINE=InnoDB` 与 `COMMENT`,每个字段必须有 `COMMENT` 说明业务含义
- **[必须]** 禁止使用外键与级联:高并发下外键强阻塞、易引发更新风暴,约束由应用层保证
- **[必须]** 禁止用存储过程、触发器、视图承载核心业务逻辑(逻辑必须写在应用层)
- **[必须]** 大表 DDL 必须用在线变更工具(`gh-ost` / `pt-online-schema-change`),禁止直接 ALTER 大表;小表 DDL 用 `ALGORITHM=INPLACE`
- **[必须]** 时间字段一律用**正整数整数时间戳**存储:默认 `BIGINT UNSIGNED` 存 Unix 秒(UTC);`created_at` / `updated_at` 固定为秒级,其余时间字段在业务要求毫秒精度时,可用 `BIGINT UNSIGNED` 存 Unix 毫秒(列名不加后缀);每个字段的秒 / 毫秒语义确定后固定不变,跨字段比较时注意单位一致;展示时由应用层转换时区;禁止 `DATETIME` / `TIMESTAMP` / `DATE` / 字符串(杜绝时区语义分歧);纯日期场景(如生日)同样用时间戳表达
### 2.2 表设计
- **[必须]** 每表必须有主键:`id INT UNSIGNED AUTO_INCREMENT`(默认);预计行数接近 21 亿(signed INT 上限,保守阈值)的高增长表(如 message 消息表)使用 `id BIGINT UNSIGNED AUTO_INCREMENT`;主键仅 `id` 单字段;禁止 UUID / 字符串 / 业务唯一键替代主键(索引膨胀、耦合业务),业务唯一键一律用唯一索引表达;分布式强需求使用 UUID 主键须评审特批
- **[必须]** 每表必备三字段:
- `id`(主键)
- `created_at BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间(Unix 秒,0 表示未设置)'`
- `updated_at BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间(Unix 秒,应用层维护,0 表示未设置)'`
- 命名全库统一,禁止混用 `create_time` / `update_time` 等其他写法
- 全库时间统一为**正整数 Unix 时间戳(`BIGINT UNSIGNED`)**:默认秒级(`0` 表示未设置);`created_at` / `updated_at` 固定秒级,其余时间字段业务要求毫秒精度时使用毫秒级(列名不加后缀);存储紧凑、无时区错位、比较与排序高效;展示格式化(`Y-m-d H:i:s`)在应用层完成;`updated_at` 由应用层(ORM 时间戳机制)维护,不使用 `ON UPDATE CURRENT_TIMESTAMP`
- **[必须]** 业务唯一性用唯一约束 / 唯一索引表达(`UNIQUE KEY`),并在应用层返回友好冲突提示
- **[必须]** 逻辑删除优先于物理删除:统一 `is_deleted TINYINT(1) NOT NULL DEFAULT 0`(全库唯一口径,禁止改用其他字段表达逻辑删除);须处理唯一键复用(如「手机号唯一」在记录已删除后的冲突策略)
- **[必须]** 表结构满足已声明的业务约束:如「密码绝不落明文」→ 表里只能有哈希列;「验证码哈希存储」→ 不能存在明文列
- **[必须]** 禁止将密码、令牌、验证码等凭证明文入库;敏感个人信息(证件号、银行卡号等)统一明文落库,接口只返回脱敏值
- **[必须]** 所有字段 `NOT NULL` + 合理默认值(仅业务必须可空时允许 NULL,如可选手机号)
- **[必须]** 状态字段用 `TINYINT` / `INT`(按取值范围选择)+ 应用层常量映射,取值不设上限,禁止无注释魔法值
### 2.3 命名规范
- **[必须]** 库名 / 表名 / 字段名:小写字母 + 数字 + 下划线,见名知意,**不超过 30 字符**,禁止拼音;表名用 `snake_case` 复数并带业务/模块前缀(如 `user_devices`、`login_logs`)
- **[必须]** 字段名:`snake_case`,语义完整清晰(如 `refresh_token_hash` 而非 `rtk`)
- **[必须]** 禁用 MySQL 保留字(`desc` / `range` / `match` / `key` / `order` 等),无法避免时用反引号
- **[必须]** 临时表 `tmp_xxx_YYYYMMDD`,备份表 `bak_xxx_YYYYMMDD`
- **[必须]** 索引命名:主键用 `PRIMARY KEY`;唯一索引 `uk_<字段>`、普通索引 `idx_<字段>`,跨表/多字段场景加表名缩写(如 `uk_user_phone`、`idx_user_created`)
- **[必须]** 同一字段在多表中的类型与命名必须完全一致(避免隐式转换致索引失效)
- **[应该]** 外键约束命名 `fk_<表>_<引用表>`(本规范禁用外键,此条仅约束遗留场景)
### 2.4 字段类型
- **[必须]** 按场景选择类型,禁止越权使用:
| 场景 | 必须使用 | 禁止使用 |
|---|---|---|
| 布尔 | `TINYINT(1)`,取值只允许 0/1 | — |
| 状态 / 枚举 | `TINYINT` 或 `INT`(按取值范围),取值不设上限,配应用层常量 | `ENUM` / 英文枚举值 |
| 小整数 | `TINYINT` / `SMALLINT` | — |
| 常规整数 | `INT` | — |
| 主键 | `INT UNSIGNED AUTO_INCREMENT`(高增长表如 message 用 `BIGINT UNSIGNED`) | UUID / 字符串主键 |
| 金额 / 汇率 | `DECIMAL(M,2)`,M 按业务定 | `FLOAT` / `DOUBLE`(精度丢失) |
| 定长短字符串 | `CHAR(N)` | — |
| 不定长字符串 | `VARCHAR(N)`,N ≤ 5000 | 一律 `VARCHAR(255)` |
| 超长文本(商品详情等) | `TEXT` / `LONGTEXT`,拆到独立表用主键关联 | 塞进主表拖垮索引 |
| 时间 | `BIGINT UNSIGNED`(默认 Unix 秒级;`created_at` / `updated_at` 固定秒级,其余字段业务要求毫秒精度时用 Unix 毫秒) | `DATETIME` / `TIMESTAMP` / `DATE`(本规范统一正整数 int 时间戳) |
- **[必须]** 证件号 / 银行卡号等定长数据不超必要长度,且落库不加密;**手机号两段式**:`country_code VARCHAR(8)`(E.164,如 `+86`)+ `phone VARCHAR(20)`(号码本体),唯一约束建在 `(country_code, phone)` 组合上
- **[必须]** 业务枚举值一律用 `TINYINT` / `INT`(按取值范围)+ 应用层常量映射;禁止 `ENUM` 与英文枚举值(如 `status = 'ACTIVE'`)
### 2.5 索引规范
- **[必须]** 单表索引 ≤ 5 个,单索引字段 ≤ 5 个
- **[必须]** 区分度低的列(`gender` / `status` / `is_deleted`)禁止单独建索引,可作组合索引后缀
- **[必须]** 组合索引遵循最左前缀:建立 `(a,b,c)` 即覆盖 `(a)` / `(a,b)` / `(a,b,c)`,避免重复建设
- **[必须]** WHERE / ORDER BY / GROUP BY 的字段应建索引,避免 `filesort`
- **[必须]** JOIN 列数据类型必须相同且都建索引,小表驱动大表
- **[必须]** 索引列上禁止函数运算与隐式类型转换(`WHERE DATE(created_at)=...`、字符串列查数字),会导致索引失效
- **[必须]** 唯一性约束用唯一索引,并作为并发写入的最终兜底
- **[必须]** 禁止冗余索引与重复索引(如已有 `idx(a,b)` 又建 `idx(a)`);新查询先 `EXPLAIN` 验证执行计划
- **[应该]** 大文本字段索引用前缀索引(`INDEX idx_title(title(100))`)
- **[应该]** 建索引前用 `SELECT COUNT(DISTINCT col) / COUNT(*)` 评估区分度
- **[应该]** 高频查询构造覆盖索引(`SELECT` 列包含在索引中)减少回表
### 2.6 SQL 编写规范
- **[必须]** 禁止 `SELECT *`,必须明确列出字段(减少 IO 与耦合):
-- 反例
SELECT * FROM orders WHERE user_id = 1000;
-- 正例
SELECT id, order_no, status, amount FROM orders WHERE user_id = 1000;
- **[必须]** INSERT 必须指定字段,禁止 `INSERT INTO t VALUES (...)` 无列名写法
- **[必须]** 深分页禁止 `LIMIT offset, size`,用游标分页:
-- 反例(深分页全扫)
SELECT * FROM orders ORDER BY id DESC LIMIT 1000000, 20;
-- 正例(游标)
SELECT id, order_no FROM orders WHERE id < ? ORDER BY id DESC LIMIT 20;
- **[必须]** WHERE 索引列禁止函数 / 运算:
-- 反例(索引列套函数)
WHERE YEAR(FROM_UNIXTIME(created_at)) = 2026
-- 正例(半开区间,参数绑定)
WHERE created_at >= ? AND created_at < ? -- ? 为应用层传入的 Unix 时间戳
- **[必须]** 禁止负向查询(`!=` / `NOT IN` / `NOT LIKE`)与 `%xxx` 前缀模糊查询(致全表扫描);优先等值 / 范围查询
- **[必须]** `IN` 列表 < 1000 项,超量改用 JOIN 或临时表
- **[必须]** 禁止 `ORDER BY RAND()`;禁止大表 JOIN 大表;避免子查询(用 JOIN 替代)
- **[必须]** 时间范围统一用半开区间 `>= AND <`,禁止 `BETWEEN` / `<=`(均含右边界,边界语义易歧义)
- **[必须]** 分页 / 批量查询必须有 `LIMIT` 上限
- **[必须]** 批量插入分批执行(单批数百~数千行),避免超大 SQL
- **[必须]** 锁范围最小化(`FOR UPDATE` 只锁必要行)
- **[必须]** 统计口径明确:`COUNT` 用明确的 `COUNT(*)` 或 `COUNT(列)`;大表计数禁止无过滤全表扫
- **[应该]** 大批量数据变更(更新/删除)分片执行,避免长时间持有锁与主从延迟
- **[应该]** 复杂聚合(趋势、分布统计)明确窗口与口径,必要时用汇总表 / 缓存结果
### 2.7 数据安全与合规
- **[必须]** 数据库账号最小权限:应用账号仅授本库 DML,管理账号与业务账号分离;禁止应用使用 root
- **[必须]** 敏感数据(个人信息)统一明文落库、接口脱敏返回、设置保留期与定期清理任务
- **[必须]** 存储的个人照片、证件扫描件等设置访问控制与生命周期(到期删除),并有清理任务实现(不允许只写注释不实现)
- **[应该]** 定期备份与恢复演练;审计表等只追加表按容量规划滚动清理/归档
- **[应该]** 慢查询日志开启,定期审查慢 SQL
### 2.8 性能与运维基线
- **[应该]** 连接使用连接池;应用与数据库间开启长连接复用
- **[应该]** 大表按时间/业务维度规划分区或归档策略
- **[应该]** 上线变更(加索引、改表)在低峰执行,DDL 使用在线算法 / 在线变更工具
### 2.9 建表 / 变更 SQL 产出流程
按以下步骤执行,每步自检通过再进入下一步:
1. 与业务确认实体、字段、关系、预期数据量与查询模式
2. 按「基础 → 命名 → 字段 → 表结构 → 索引」顺序逐条自检本规范
3. 输出完整 DDL(含 COMMENT、默认值、索引、引擎、字符集)
4. 附带 migration 脚本,必须幂等(`IF NOT EXISTS` / `INSERT IGNORE`),本地验证「全新安装 + 升级安装」两条路径
5. 用 `EXPLAIN` 验证关键查询命中索引:`type` 应为 `const` / `ref` / `range`,禁止 `ALL`
---
## 3. 评审入口与提交前检查清单
代码 / DDL 提交评审时逐条核对:
- [ ] 所有表、字段有 COMMENT,注释说明业务含义
- [ ] 主键存在且为 `INT UNSIGNED AUTO_INCREMENT`(高增长表为 `BIGINT UNSIGNED`),主键仅 `id` 单字段
- [ ] `id` / `created_at` / `updated_at` 三字段齐全(时间字段为 `BIGINT UNSIGNED` 时间戳),命名全库统一
- [ ] 无外键、无冗余索引、无重复索引、无保留字
- [ ] 金额字段为 `DECIMAL`,无 `FLOAT` / `DOUBLE` 存金额
- [ ] 所有字段 `NOT NULL` 有默认值(业务必须可空除外)
- [ ] 索引区分度已评估;单表索引 ≤ 5,区分度低列未单独建索引
- [ ] 无 `SELECT *`、无字符串拼接 SQL、无深分页 `OFFSET`、无负向查询
- [ ] migration 脚本幂等且本地通过(全新安装 + 升级安装)
- [ ] 关键 SQL 已 `EXPLAIN` 验证(type 非 ALL)
- [ ] 注释 / 文档声称与实现一致(声称加密、声称即删必须真实实现)
- [ ] 唯一性由数据库约束兜底,无「先查后插」;一次性资源消费原子化
- [ ] 无明文凭证数据(密码 / 令牌 / 验证码);敏感个人信息(证件号 / 银行卡号 / 手机号)接口已脱敏返回
### 禁止做的事(红线)
- 不要生成带外键的 DDL
- 不要用 `FLOAT` / `DOUBLE` 存金额
- 不要用 `SELECT *`
- 不要用字符串列做主键
- 不要在索引列上套函数
- 不要用存储过程 / 触发器 / 视图承载核心业务逻辑
- 不要一次性给出大批量未经自检的 DDL,必须逐表自检后再输出
- 不要硬编码密钥与默认密钥上线