开发规范
命名规范
ThinkPHP 8.0 遵循 PSR-2 命名规范和 PSR-4 自动加载规范。
目录和文件
| 规则 | 示例 |
|---|---|
| 目录使用小写+下划线 | controller/, user_service/ |
类库、函数文件统一以 .php 为后缀 | UserController.php |
| 类的文件名与命名空间路径一致 | app\auth\controller\AuthController → app/auth/controller/AuthController.php |
| 类文件采用驼峰法命名(首字母大写) | UserController.php, UserType.php |
| 其它文件采用小写+下划线命名 | common.php, route.php |
类命名
| 规则 | 示例 |
|---|---|
| 类名采用驼峰法(首字母大写) | User, UserType, AuthController |
| 类名与文件名保持一致 | UserController 类 → UserController.php |
方法和属性命名
| 规则 | 示例 |
|---|---|
| 方法采用驼峰法(首字母小写) | getUserName(), getUserById() |
| 属性采用驼峰法(首字母小写) | $tableName, $instance |
| 魔术方法以双下划线开头 | __call(), __autoload() |
函数命名
| 规则 | 示例 |
|---|---|
| 函数使用小写字母和下划线 | get_client_ip(), array_to_tree() |
常量和配置
| 类型 | 规则 | 示例 |
|---|---|---|
| 常量 | 大写字母和下划线 | APP_PATH, HAS_ONE |
| 配置参数 | 小写字母和下划线 | url_route_on, url_convert |
| 环境变量 | 大写字母和下划线 | APP_DEBUG, DB_HOST |
数据表和字段
| 规则 | 示例 |
|---|---|
| 数据表采用小写+下划线 | think_user, sys_role_menu |
| 字段采用小写+下划线 | user_name, created_at |
| 禁止使用驼峰和中文命名 | ❌ userName, ❌ 用户表 |
项目结构
app/
├─ auth/controller/ # 认证模块
├─ system/ # 系统模块(controller/service/model/enums/validate)
├─ codegen/ # 代码生成
├─ file/ # 文件上传
├─ common/ # 公共模块
│ ├─ constants/ # 常量定义
│ ├─ enums/ # 枚举定义
│ ├─ exception/ # 异常类
│ ├─ middleware/ # 中间件(Auth/Perm/DataScope/RateLimit/Log)
│ ├─ model/ # 基础模型
│ ├─ traits/ # Trait 复用
│ ├─ util/ # 工具类
│ ├─ validate/ # 验证器
│ └─ web/ # Web 响应/基类
extend/
├─ jwt/ # JWT Token 管理
├─ sse/ # SSE 实时通信
├─ redis/ # Redis 客户端
└─ http/ # HTTP 工具
config/ # 配置文件分层架构规约
调用链路
各层职责
| 层级 | 职责 | 禁止事项 |
|---|---|---|
| Controller | 参数校验、调用 Service、组装返回结果 | ❌ 禁止直接操作数据库 |
| Service | 业务逻辑、事务控制、数据组装 | ❌ 禁止跨业务调用 Model |
| Model | 数据库访问、ORM 操作 | ❌ 禁止包含业务逻辑 |
Service 调用规范
核心原则:Service 只能调用自己业务域的 Model,禁止跨业务调用。
php
// ✅ 正确:UserService 调用 User Model
final class UserService
{
public function getUserById(int $id): ?User
{
return User::find($id); // 自己业务域
}
}
// ❌ 错误:UserService 直接调用 Role Model
final class UserService
{
public function getUserWithRoles(int $id): ?User
{
$user = User::find($id);
$roles = Role::where('user_id', $id)->select(); // ❌ 错误
return $user;
}
}
// ✅ 正确:通过 RoleService 获取角色
final class UserService
{
public function getUserWithRoles(int $id): ?User
{
$user = User::find($id);
$roleService = app()->make(RoleService::class);
$roles = $roleService->getRolesByUserId($id); // ✅ 正确
return $user;
}
}跨业务数据交互
| 场景 | 正确做法 |
|---|---|
| UserService 需要角色数据 | 调用 RoleService::getRolesByUserId() |
| OrderService 需要用户数据 | 调用 UserService::getUserById() |
| 需要多个业务数据组装 | 在 Service 层调用多个 Service 组装 |
RESTful API 规范
URL 设计
| 规则 | 示例 | 说明 |
|---|---|---|
| 使用名词复数 | /api/v1/users | ✅ 正确 |
| 避免动词 | /api/v1/getUsers | ❌ 错误 |
| 层级不超过 3 层 | /api/v1/users/{id}/roles | ✅ 正确 |
| 使用连字符分隔 | /api/v1/user-profiles | ✅ 正确 |
HTTP 方法映射
| HTTP 方法 | 操作 | URL 示例 | 权限标识 |
|---|---|---|---|
GET | 列表查询 | GET /api/v1/users | sys:user:list |
GET | 单个查询 | GET /api/v1/users/{id} | sys:user:list |
POST | 新增 | POST /api/v1/users | sys:user:create |
PUT | 全量修改 | PUT /api/v1/users/{id} | sys:user:update |
PATCH | 部分修改 | PATCH /api/v1/users/{id}/status | sys:user:update |
DELETE | 删除 | DELETE /api/v1/users/{id} | sys:user:delete |
HTTP 状态码规范
核心原则
HTTP 状态码表示 HTTP 请求是否成功到达并被处理,业务错误不属于 HTTP 层面的错误。
状态码对照表
| HTTP | 场景 | 说明 |
|---|---|---|
| 200 | 业务错误 | 验证码错误、密码错误、用户不存在、数据校验失败等 |
| 200 | 成功 | 请求成功,业务正常执行 |
| 401 | 未认证 | 未登录、Token 无效或过期 |
| 403 | 无权限 | 已登录但无接口访问权限(RBAC 检查失败) |
| 404 | 资源不存在 | 接口路径不存在 |
| 500 | 服务器错误 | 数据库异常、空指针、未捕获异常等 |
三种"无权限"的区分
| 场景 | HTTP | 说明 |
|---|---|---|
| 没登录 | 401 | 没有身份 |
| 登录了但没有接口权限 | 403 | 没有接口访问权 |
| 有权限访问接口,但业务校验不通过 | 200 | 业务失败 |
响应格式示例
json
// 成功 - HTTP 200
{
"code": "00000",
"msg": "成功",
"data": { ... }
}
// 业务错误 - HTTP 200
{
"code": "A0240",
"msg": "验证码错误",
"data": null
}
// 未认证 - HTTP 401
{
"code": "A0230",
"msg": "访问令牌无效",
"data": null
}
// 无权限 - HTTP 403
{
"code": "A0301",
"msg": "访问未授权",
"data": null
}
// 服务器错误 - HTTP 500
{
"code": "B0001",
"msg": "系统执行出错",
"data": null
}为什么业务错误返回 200
- HTTP 语义:HTTP 200 表示服务器成功处理了请求,只是业务结果不符合预期
- 前端处理:前端可以根据响应体中的
code字段判断具体错误类型,统一显示错误提示 - 日志记录:业务错误通常不需要触发 HTTP 错误日志,减少监控噪音
- 主流方案:大多数后台管理系统采用此方案,便于前后端协作
接口示例
php
// 路由定义
Route::group('api/v1', function () {
Route::get('users', 'system.user/list'); // 列表查询
Route::get('users/:id', 'system.user/read'); // 单个查询
Route::post('users', 'system.user/save'); // 新增
Route::put('users/:id', 'system.user/update'); // 修改
Route::delete('users/:id', 'system.user/delete'); // 删除
Route::patch('users/:id/status', 'system.user/updateStatus'); // 部分修改
});权限标识规范
命名格式
模块:资源:操作操作命名对照表
| 操作 | 标识 | HTTP 方法 | 说明 |
|---|---|---|---|
| 列表查询 | list | GET | 分页/列表查询 |
| 新增 | create | POST | 创建资源 |
| 修改 | update | PUT/PATCH | 更新资源 |
| 删除 | delete | DELETE | 删除资源 |
| 导入 | import | POST | 批量导入 |
| 导出 | export | GET | 数据导出 |
| 发布 | publish | POST | 发布操作 |
| 撤销 | revoke | POST | 撤销操作 |
权限标识示例
php
// 用户管理
sys:user:list // 列表
sys:user:create // 新增
sys:user:update // 修改
sys:user:delete // 删除
sys:user:import // 导入
sys:user:export // 导出
sys:user:reset-password // 重置密码
// 角色管理
sys:role:create
sys:role:update
sys:role:delete
sys:role:assign // 分配权限
// 通知管理
sys:notice:list
sys:notice:create
sys:notice:update
sys:notice:delete
sys:notice:publish // 发布
sys:notice:revoke // 撤销错误码规范
设计原则
参考《阿里巴巴 Java 开发手册》错误码规范:
- 快速溯源:通过错误码快速定位错误来源
- 简单易记:5 位字符串,易于记忆和比对
- 沟通标准化:脱离文档也能准确沟通
错误码结构
错误来源(1位)+ 数字编号(4位)| 来源 | 前缀 | 说明 |
|---|---|---|
| 用户端错误 | A | 参数错误、认证失败、权限不足等 |
| 系统错误 | B | 系统超时、内部异常等 |
| 第三方服务 | C | 数据库、中间件、外部 API 等 |
号段划分
| 号段 | 分类 | 示例 |
|---|---|---|
A0001 | 一级宏观 | 用户端错误 |
A0100 | 二级宏观 | 用户注册错误 |
A01xx | 三级细分 | A0101 用户未同意协议 |
A0200 | 二级宏观 | 用户登录异常 |
A02xx | 三级细分 | A0230 Token 无效 |
常用错误码
| 错误码 | 说明 | 场景 |
|---|---|---|
00000 | 成功 | 正常执行 |
A0001 | 用户端错误 | 一级宏观 |
A0200 | 用户登录异常 | 登录失败 |
A0230 | Token 无效或过期 | 认证失败 |
A0301 | 访问未授权 | 权限不足 |
A0400 | 请求参数错误 | 参数校验失败 |
A0506 | 请勿重复提交 | 防重复提交 |
B0001 | 系统执行出错 | 一级宏观 |
C0001 | 第三方服务出错 | 一级宏观 |
使用示例
php
// 定义错误码常量
final class ResultCode
{
public const SUCCESS = '00000';
public const USER_ERROR = 'A0001';
public const USER_LOGIN_ERROR = 'A0200';
public const TOKEN_INVALID = 'A0230';
public const ACCESS_UNAUTHORIZED = 'A0301';
public const PARAM_ERROR = 'A0400';
public const DUPLICATE_SUBMISSION = 'A0506';
public const SYSTEM_ERROR = 'B0001';
}
// 使用示例
if (!$user) {
throw new BusinessException(ResultCode::USER_ERROR, '用户不存在');
}
// 响应格式
{
"code": "A0001",
"msg": "用户不存在",
"data": null
}排序字段规范
字段命名
| 字段名 | 类型 | 说明 |
|---|---|---|
sort | int | 排序号(升序,值越小越靠前) |
create_time | datetime | 创建时间(降序) |
update_time | datetime | 更新时间(降序) |
排序规则
php
// ✅ 推荐:先按 sort 升序,再按创建时间降序
User::order('sort', 'asc')
->order('create_time', 'desc')
->select();
// 示例:角色列表排序
Role::order('sort', 'asc')
->order('create_time', 'desc')
->order('update_time', 'desc')
->select();
// 示例:菜单列表排序
Menu::order('sort', 'asc')->select();默认排序
| 业务场景 | 默认排序 |
|---|---|
| 列表查询 | sort ASC, create_time DESC |
| 树形结构 | sort ASC |
| 日志记录 | create_time DESC |
代码注释规范
类注释
php
/**
* 用户业务服务类
*
* @author Ray.Hao
* @since 2024/1/14
*/
final class UserService
{
// ...
}方法注释
php
/**
* 获取用户分页列表
*
* @param array $params 查询参数
* @return array 用户分页列表
*/
public function getPageList(array $params): array
{
// ...
}
/**
* 新增用户
*
* @param array $data 用户表单数据
* @return bool 是否新增成功
*/
public function saveUser(array $data): bool
{
// ...
}属性注释
php
final class User extends Model
{
/** @var string 用户ID */
protected $id;
/** @var string 用户名 */
protected $username;
/** @var int 用户状态:1-启用,0-禁用 */
protected $status;
}日志规范
日志级别
| 级别 | 场景 |
|---|---|
error | 错误信息,需要立即处理 |
warning | 警告信息,可能存在问题 |
info | 关键流程信息 |
debug | 调试信息 |
日志格式
php
// ✅ 推荐:使用结构化日志
Log::info('用户登录成功', [
'userId' => $userId,
'username' => $username,
'ip' => request()->ip()
]);
Log::error('用户登录失败', [
'username' => $username,
'reason' => $e->getMessage()
]);
// ❌ 不推荐:字符串拼接
Log::info("用户登录成功:userId={$userId}");敏感信息脱敏
php
// ✅ 推荐:敏感信息脱敏
Log::info('用户登录', [
'mobile' => maskMobile($mobile)
]);
// ❌ 不推荐:打印敏感信息
Log::info('用户登录', [
'password' => $password // ❌ 禁止打印密码
]);最佳实践
避免魔法值
php
// ❌ 不推荐
if ($user->status == 1) { }
// ✅ 推荐:使用枚举
if ($user->status == UserStatus::ENABLED) { }
// ✅ 推荐:使用常量
final class UserStatus
{
public const ENABLED = 1;
public const DISABLED = 0;
}数组判空
php
// ❌ 不推荐
if (count($list) > 0) { }
// ✅ 推荐
if (!empty($list)) { }字符串判空
php
// ❌ 不推荐
if ($str != null && strlen($str) > 0) { }
// ✅ 推荐
if (!empty($str)) { }避免空指针
php
// ❌ 不推荐
return $user->username == 'admin';
// ✅ 推荐
return $user && $user->username == 'admin';
// ✅ 推荐:使用空合并运算符
return $user->username ?? '' == 'admin';相关文件
| 文件 | 说明 |
|---|---|
app/common/constants/ResultCode.php | 错误码常量定义 |
app/common/exception/BusinessException.php | 业务异常类 |
app/common/exception/Handle.php | 全局异常处理 |
app/common/web/BaseController.php | 控制器基类 |
app/common/web/Response.php | 统一响应封装 |
控制器规范
php
final class UserController extends BaseController
{
public function list(): Json
{
$service = $this->app->make(UserService::class);
return $this->success($service->getPageList($this->request->get()));
}
}要点:
- 继承
BaseController,使用$this->success()返回 - 禁止直接操作数据库,必须通过 Service
- 使用
declare(strict_types=1)开启严格类型
服务规范
php
final class UserService
{
public function getPageList(array $params): array
{
$query = User::where('is_deleted', 0);
return ['list' => $query->page($params['page'] ?? 1, $params['pageSize'] ?? 10)->select(), 'total' => $query->count()];
}
}参数校验
php
final class UserValidate extends Validate
{
protected $rule = ['username' => 'require|max:50', 'password' => 'require|min:6'];
protected $message = ['username.require' => '用户名不能为空'];
}
// 控制器中使用
$this->validate($this->request->post(), UserValidate::class);统一响应
php
// 成功: {"code": "00000", "msg": "成功", "data": ...}
// 分页: {"code": "00000", "msg": "成功", "data": {"list": [...], "total": 100}}
// 错误: {"code": "A0400", "msg": "参数错误", "data": null}异常处理
php
// 抛出业务异常
throw new BusinessException(ResultCode::USER_NOT_FOUND, '用户不存在');
// 全局异常处理器自动捕获并返回标准响应接口幂等
- 优先使用数据库唯一约束兜底
- 高风险写操作使用 Redis 分布式锁
php
$lockKey = "lock:order:{$orderId}";
if (!Redis::setnx($lockKey, 1, 30)) throw new BusinessException(ResultCode::DUPLICATE_SUBMISSION);
try { /* 业务处理 */ } finally { Redis::del($lockKey); }注意事项
- 避免使用 PHP 保留字:禁止使用
class,function,if等保留字作为类名、方法名、命名空间 - 大小写敏感:在类 Unix 系统上,文件名大小写敏感,确保命名空间与文件路径一致
- 调试模式:ThinkPHP 调试模式下会严格检查大小写
