## 关于后端代码结构统一的约定
## **前言**
在现代软件工程的广阔疆域中,代码质量已牢固确立为衡量项目稳健性、可维护性和团队协作效能的核心标尺。为铸就坚实、明晰且易于传承的代码宝典,制定一套系统化、高标准的编码规范与共识显得尤为迫切。本《代码结构统一度约定手册》应运而生,旨在为我司开发团队铺设一条规范化的编程实践之路,引导我们在代码风格塑造、模块架构布局、逻辑表述艺术及数据库蓝图绘制等诸多维度达成一致性与最佳实践的共鸣。此文档不仅旨在抛砖引玉,更是诚挚邀请每一位团队成员积极献策,共襄代码美学与工程智慧的盛宴。您的真知灼见,必将点亮我们共筑卓越代码殿堂的璀璨星光。
## **编程规约**
**命名风格**
强制 文件命名: 文件名称应遵循“大驼峰”(Upper Camel Case)命名规则。这意味着每个单词的首字母均大写,且单词之间无空格或任何其他分隔符。例如:
```
RequestHepler.php
UserAuthenticationModel.php
DatabaseConnectionConstant.php
```
强制 代码变量: 在PHP代码中,变量名称应采用“小驼峰”(lower camel case)命名法。即首个单词以小写字母开始,后续单词的首字母大写,其余字母均为小写。示例:
```
$requestData
$userProfile
$databaseConnection
```
强制 数组键名: 数组键应当使用全小写字母,并通过下划线 \_ 连接单词,形成所谓的“蛇形命名”(snake\_case)。以下是一些示例:
```
$userInfo = [
'first_name' => 'John',
'last_name' => 'Doe',
'email' => 'john.doe@example.com',
];
$productDetails = [
'product_id' => 123,
'product_name' => 'Example Product',
'price' => 99.99,
];
```
推荐 常量命名:建议采用全大写命名法,并使用下划线(\_)进行单词分隔。
```
const PAGE_SOURCE_LIBRARY = 14;
const PAGE_SOURCE_DISCOVER = 18;
const PAGE_SOURCE_PROFILE = 19;
```
常量文件建议写入Constant文件中,和表强关联的常量可以写入表对应的MODEL文件中(不推荐),RedisKey定义常量文件写入入RedisKey文件中。
推荐 缓存命名:建议采用“蛇形命名”,变量部分采用:分隔开。前半部分建议和常量名称保持一致。
```
const ABROAD_GOOGLE_ACCESS_TOKEN = 'abroad_google_access_token:%s';
const ABROAD_GOOGLE_REFRESH_TOKEN = 'abroad_google_refresh_token:%s';
```
强制 杜绝完全不规范的缩写,避免望文不知义。
反例:AbstractClass“缩写”命名成 AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性
**代码格式**
强制 方法约定:方法名应该为小驼峰形式,换行格式如下:
```
public function getCreatedAtAttribute(string $value) : string
{
return date('Y-m-d H:i:s', strtotime($value));
}
```
方法外部的大括号均另起一行。
推荐 参数类型声明:为函数或方法的每个参数明确指定其数据类型。此举有助于编译器/解释器进行类型检查,提前发现潜在的类型错误,同时显著提升代码的可读性与自文档化特性。
推荐 尽量避免一个方法在不同条件下返回不同类型的值。这种设计容易造成类型推断困难,增加调用者的使用复杂性,且不利于静态类型检查。如有必要,可考虑拆分为多个明确职责的方法。
推荐 为函数或方法声明其返回值的数据类型。这将进一步强化代码的契约性,使调用者清楚了解预期的返回结果类型,降低误解与错误使用风险。
推荐 区分数组和MAP的命名,假如一个数组不带KEY,可以用s\\list结尾,例如 $books, $userList,但是假如数组存在KEY,建议用map结尾,例如 $channelMap。这样能方便后边使用的时候判断,例如存在$books\[$bookId\]的写法,大概率就是有问题的。假如是一个单独的对象,如一本书,建议用单数名称表示,例如 $book或者加上Info,例如 $bookInfo。
强制 大括号的使用约定:在程序中进行结构控制代码编写,如if、for、while、switch等结构,如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则:
1) 左大括号前不换行。
2) 左大括号后换行。
3) 右大括号前换行。
4) 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行
强制 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格。。
反例:if (空格 a == b 空格)
强制 if/for/while/switch/do 等保留字与括号之间都必须加空格。
强制 任何二目、三目运算符的左右两边都需要加一个空格。
说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。
强制 注释的双斜线与注释内容之间有且仅有一个空格。
<span style="color:green;">**正例:**</span>
```
// 这是示例注释,请注意在双斜线之后有一个空格
$a = 1;
```
强制 方法参数在定义和传入时,多个参数逗号后边必须加空格。
<span style="color:green;">**正例:**</span>下例中实参的 $recommend,后边必须要有一个空格。
```
protected function getTopics($recommend, $recommendBooks, $recommendIds): array
{
... 代码部分省略
}
```
<span style="color:red;">**强制**</span>
IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式,不要使用 Windows 格式。
强制 禁止写大而全的方法,这样的功能过于复杂、耦合性高、不易测试、难于维护、可读性差。为改善这种情况,应遵循“单一职责原则”(SRP)对方法进行拆分重构,将每个独立的逻辑块封装成专门的方法。将方法被拆分为多个职责单一的小方法,每个方法专注于一项具体任务。这样既提高了代码的可读性、可测试性和可维护性,又降低了出错的概率。
<span style="color:red;">**反例:**</span>
```
public static function handleOrder($orderNum, $grades, $isTest = 0)
{
$order = Order::*selectInfo*([ 'order_num' => $orderNum]);
// 修改订单状态
// ... 这里是一堆修改订单状态的代码
// 修改用户充值状态
// ... 这里是一堆修改充值状态的代码
// 发放充值奖励
// ... 这里是一堆发放充值奖励的代码
// 延长染色时长
// ... 这里是一堆延长染色时长的代码
// ..其他的代码
}
```
<span style="color:green;">**推荐**</span>
不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。
说明:任何情形,没有必要插入多个空行进行隔开。也不要每一行都加个空行,就失去了提升的意义。
控制语句
<span style="color:red;">**强制**</span>
在一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
说明:注意 break 是退出 switch 语句块,而 return 是退出方法体。
<span style="color:red;"> **强制**</span>
在 if/else/for/while/do 语句中必须使用大括号。 说明:即使只有一行代码,也禁止不采用大括号的编码方式。
<span style="color:red;"> **反例:**</span>
if (condition) statements;
<span style="color:red;"> **强制 **</span>
在高并发场景中,避免使用”等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件 来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数, 这样的话,活动无法终止。
推荐 当某个方法的代码总行数超过 10 行时,return / throw 等中断逻辑的右大括号后均 需要加一个空行。 说明:这样做逻辑清晰,有利于代码阅读时重点关注。
推荐 表达异常的分支时,少用 if-else 方式,这种方式可以改写成:
```
if ($condition) {
return $obj;
}
// 接着写 else 的业务逻辑代码;
```
说明:如果非使用 if()...else if()...else...方式表达逻辑,避免后续代码维护困难,请勿超过 3 层。 正例:超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句 示例如下:
```
function findBoyfriend($man) {
if ($man->isUgly()) {
echo "本姑娘是外貌协会的资深会员\\n";
return;
}
if ($man->isPoor()) {
echo "贫贱夫妻百事哀\\n";
return;
}
if ($man->isBadTemper()) {
echo "银河有多远,你就给我滚多远\\n";
return;
}
echo "可以先交往一段时间看看\\n";
}
```
<span style="color:green;"> 推荐</span>
除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复 杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
说明:很多 if 语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
<span style="color:green;">**正例**</span>:
```
if (ControlGroupHelper::*isControlGroup*($userId, 1, [0, 2, 4, 6, 8])) {
return $this->getContinueReadPopUp($userId, 1);
}
```
<span style="color:red;">**反例**</span>:
```
if (!tryAcquire($arg) && acquireQueued(addWaiter(Node::EXCLUSIVE), $arg)) {
selfInterrupt();
}
```
<span style="color:green;">**推荐** </span>
不要在其它表达式(尤其是条件表达式)中,插入赋值语句。 说明:赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。
反例:
```
// 算术表达式中出现赋值操作,容易忽略 $count 值已经被改变
$threshold = ($count = PHP_INT_MAX) - 1;
// 条件表达式中出现赋值操作,容易误认为是 $sync==$fair
return ($sync = $fair) ? new FairSync() : new NonfairSync();
```
<span style="color:green;">**推荐 **</span>
避免采用取反逻辑运算符。
说明:取反逻辑不利于快速理解,并且取反逻辑写法一般都存在对应的正向逻辑写法。
<span style="color:green;">**正例:**</span>
使用 if (x < 628) 来表达 x 小于 628。
<span style="color:green;">**反例:**</span>
使用 if (!(x >= 628)) 来表达 x 小于 628。
参考 下列情形,需要进行参数校验:
1) 调用频次低的方法。
2) 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致 中间执行回退,或者错误,那得不偿失。
3) 需要极高稳定性和可用性的方法。
4) 对外提供的开放接口,不管是 RPC/API/HTTP 接口。
5) 敏感权限入口。
参考 下列情形,不需要进行参数校验:
1) 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查。
2) 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题,可以省略。
3) 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。
注释规约
强制 类方法的注释必须使用 PHPDoc 规范,使用/\*\*内容\*/格式,不得使用 // xxx 方式。
说明:在 IDE 编辑窗口中,PHPDoc 方式会提示相关注释,生成 PHPDoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
<span style="color:green;">**正例:**</span>
```
/**
* 简短描述
*
* 详细描述可以包括多行,解释函数的作用、参数和返回值。
*
* @param string $param1 描述参数1
* @param int $param2 描述参数2
* @return bool 返回值的描述
*/
function example($param1, $param2) {
// 函数体
}
```
<span style="color:red;">**强制:**</span>
方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使 用/\* \*/注释,注意与代码对齐。
<span style="color:green;">**推荐:**</span>
代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑 等的修改。
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了 导航的意义。
参考 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一 个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释又是相当大的负担。
<span style="color:red;">**反例:**</span>
```
// 将大象放入冰箱
put($elephant, $fridge);
```
方法名 put,加上两个有意义的变量名 elephant 和 fridge,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。
前后端规约
<span style="color:green;">**推荐:**</span>
服务端返回的数据,使用 JSON 格式而非 XML。
<span style="color:green;">**推荐:**</span>
在前后端数据交互过程中,特别是在传递布尔型参数时,我们应当谨慎对待以避免潜在的数据类型混淆问题。由于PHP作为一种动态类型且类型检查相对宽松的语言,直接传递true或false作为布尔值可能会遭遇意料之外的转化行为,导致逻辑判断失误。具体而言,当布尔值在传递过程中被不恰当地转化为字符串形式,如"true"或"false",PHP在处理时可能会将这些字符串误解为其对应的非空真值,从而使得原本意指逻辑假的"false"在条件判断中仍被视为真。
反例:本人之前对结果某银行的接口,文档中说明有个字段返回的是bool类型,于是用empty()判断为空,哪知对方传了一个string类型的false,导致接口被判读成恒true。
强制 在服务端开发过程中,严谨区分Map(关联数组)与数组(索引数组)的数据结构至关重要。由于PHP自身的特性并不强制区分这两种类型的数组,若处理不当,极易导致在预期返回空Map的情况下错误地返回了空数组,进而引发客户端对接口响应的误解和后续逻辑处理的困扰。
<span style="color:red;">**反例:**</span>
不存在可用订阅档位的时候,为了减少查询,服务端不打算返回订阅档位信息,于是将数组中的字段置空,导致客户端收到了空数组,造成无法解析的问题。
说明:在实际操作中,可以用stdClass来返回空对象
```
function getUserPreferences($userId) {
// 查询用户偏好数据...
if (!$preferencesFound) {
// 如果没有找到用户偏好,返回一个空对象
return new stdClass();
}
// 否则,返回填充了数据的用户偏好对象
return $populatedUserPreferencesObject;
}
```
<span style="color:green;">**推荐:**</span>
对于需要使用超大整数的场景,服务端建议使用 String 字符串类型返回。 防止出现浮点型导致的精度丢失情况。
<span style="color:red;">**强制:**</span>
无论是新接口还是旧接口改造,服务端都需要及时维护文档,约定响应数据接口和对应的逻辑。同样客户端也需要仔细阅读文档,有问题及时提出。
<span style="color:green;">**推荐:**</span>
客户端提交的请求参数,作为系统交互的源头之一,本质上属于外部输入,其可信度无法预设。因此,秉持安全至上的原则,我们必须视其为潜在的不可信数据,严谨对待,以杜绝因参数误传、恶意篡改或缺失等情形引发的系统异常与安全隐患。
<span style="color:red;">**反例:**</span>
存在一个优惠券记录接口,存在参数type 1 全部 2 过期 3 已使用,结果客户端请求了 0 1 2 导致优惠券展示错位。
MySQL 数据库
**建表规约**
**• 表名**
<span style="color:red;">**强制:**</span>
表名、字段名必须使用小写字母或数字,根据项目添加或者不添加前缀,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、
表名、字段名,都不允许出现任何大写字母,避免节外生枝。
<span style="color:green;">**正例:**</span>
aliyun_admins,rdc_configs,level3_names
<span style="color:red;">**反例:**</span>
AliyunAdmin,rdcConfig,level_3_name
推荐 表的命名最好是加上“业务名称\_表的作用”。
<span style="color:green;">**正例:**</span>
alipay_tasks / force_projects / trade_configs
<span style="color:green;">**推荐:**</span>
由于laravel框架的约定,也为了保持一致,表名采用名词的复数形式,以反映表中存储的是多个同类实体的数据集合。
SQL
fq_users
• 字段名
<span style="color:red;">**强制:**</span>
字段名应使用全小写字母或数字。同样采用“蛇形命名”,即单词间通过下划线 \_ 连接。
<span style="color:red;">**强制:**</span>
禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。
<span style="color:red;">**强制:**</span>
小数类型为 decimal,禁止使用 float 和 double。
说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
<span style="color:red;">**强制:**</span>
表达是与否概念的字段,必须使用 is\_xxx 的方式命名,数据类型是 unsigned tinyint 1 表示是,0 表示否)。
说明:任何字段如果为非负数,必须是 unsigned。
<span style="color:green;">**正例:**</span>
表达逻辑删除的字段名 is\_deleted,1 表示删除,0 表示未删除。
推荐 除非字段表示“是与否概念”时,强烈建议避免采用数值0作为选项值。这样做有助于规避在使用empty()函数进行判断时可能遭遇的潜在异常情况。选用TINYINT UNSIGNED数据类型,既能有效压缩存储空间,又能赋予字段宽泛的选择范围。
推荐 表必备三字段:id, created_at, updated_at。
说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。gmt\_create, gmt\_modified 的类型均为 timestamp 类型,前者现在时表示主动创建,后者过去分词表示被动更新。
推荐 字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
1)不是频繁修改的字段。
2)不是 varchar 超长字段,更不能是 text 字段。
<span style="color:green;">**正例:**</span>
商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存储类目名称,避免关联查询。
• 索引
强制 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx\_字段名。
说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。
• 其他
<span style="color:green;">**推荐:**</span>
如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
推荐 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
<span style="color:green;">**推荐:**</span>
如果需要保存字符集推荐使用utf8mb4格式,特别是需要保存表情的场景下。使用其他字符集的可能导致表情保存失败。且不同的字符集在JOIN的时候会由于字符集和校对规则的不一致,MySQL无法正确比较和处理涉及这些字段的查询条件,出现Illegal mix of collations问题。
[该类型的内容暂不支持下载\]
MySQL索引规约
**• 唯一索引**
<span style="color:red;">**强制:**</span>
业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
**• JOIN关联**
<span style="color:red;">**强制:**</span>
API代码中超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、SQL 性能。
**• 搜索条件**
<span style="color:green;">**推荐:**</span>
页面搜索减少左模糊或者全模糊的使用,如果需要请走搜索引擎来解决。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
**• 优化的目标**
推荐 SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts最好。
说明:
1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2)ref 指的是使用普通的索引(normal index)。
3)range 对索引进行范围检索。
反例:explain 表的结果,type=index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。
**• 组合索引**
<span style="color:green;">**推荐:**</span>
建组合索引的时候,区分度最高的在最左边。
正例:如果 where a=? and b=? ,如果 a 列的几乎接近于唯一值,那么只需要单建 idx\_a索引即可。
说明:存在非等号和等号混合时,在建索引时,请把等号条件的列前置。如:where c>? and
d=? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即索引 idx_d_c。
**• 隐式转化**
推荐 防止因字段类型不同造成的隐式转换,导致索引失效。
**• 其他**
参考 创建索引时避免有如下极端误解:
1)宁滥勿缺。认为一个查询就需要建一个索引。
2)宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
3)抵制唯一索引。认为业务的唯一性一律需要在应用层通过“先查后插”方式解决。
MySQL语句
**• count(*)**
<span style="color:red;">**强制:**</span>
不要使用 count(列名)或 count(常量)来替代 count(*),count(*)是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
**• count(distinct col)**
<span style="color:red;">**强制:**</span>
count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
**• 分页**
<span style="color:red;">**强制:**</span>
在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
**• 存储过程**
<span style="color:red;">**强制:**</span>
禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
**• IN操作**
<span style="color:green;">**推荐:**</span>
API代码中in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
**• 查询字段**
<span style="color:green;">**推荐:**</span>
在表查询中,建议不要使用 \* 作为查询的字段列表,需要哪些字段最好明确写明。
说明:
1)增加查询分析器解析成本。
2)增减字段容易与 resultMap 配置不一致。
3)无用字段增加网络消耗,尤其是 text 类型的字段。
最后,祝大家没有BUG。