在ThinkPHP 6.0中需用Route::group()硬编码api/v1、api/v2前缀并清缓存,控制器目录与命名空间须全小写且严格对齐,中间件按分组绑定,服务层通过工厂或接口隔离版本逻辑。

在 ThinkPHP 6.0 项目中稳定支持 v1 和 v2 接口并行运行,避免上线后突然 404、控制器加载失败或中间件错绑,必须绕过框架对动态路由和大小写不敏感的本地开发假象——Linux 生产环境会直接拒绝大写 V1 目录和变量前缀路由。
用 Route::group() 写死路径前缀
这是唯一能与 ThinkPHP 路由缓存机制兼容的方案。TP 不解析 URL 字符串做版本分流,只认你手写的字面量前缀。
第一步:打开 route/app.php,在文件末尾添加两组独立分组:
Route::group('api/v1', function () { Route::get('user/:id', 'api/v1.User/read'); Route::post('order', 'api/v1.Order/create'); })->middleware(['cors', 'throttle:60,1']);
立即学习“PHP免费学习笔记(深入)”;
Route::group('api/v2', function () { Route::get('user/:id', 'api/v2.User/read'); Route::post('order', 'api/v2.Order/create'); })->middleware(['cors', 'throttle:100,1', 'jwt']);
【前缀不带开头斜杠,写成 'api/v1' 而非 '/api/v1'】 TP 会自动拼接,多加斜杠会导致实际匹配路径变成 //api/v1/user,触发 404。
改完立刻执行 php think route:clear ——否则旧缓存还在,新路由永不生效。
控制器目录与命名空间严格对齐
Linux 服务器区分大小写,Windows 开发时一切正常,部署后立刻报 Class apppi1User does not exist,其实类就在那里,只是 PSR-4 加载器按小写路径去找。
方法一:创建标准目录结构
在 app/ 下新建 api/v1/User.php 和 api/v2/User.php 两个文件。
方法二:确认命名空间全小写且与路径完全一致
api/v1/User.php 文件首行必须是 namespace apppi1;,类名必须是 class User;
api/v2/User.php 首行是 namespace apppi2;,类名也是 class User。
【v1 和 v2 目录名必须全小写,不能是 V1、ApiV1 或 _v1】 下划线开头的目录会被 TP 自动跳过,_v1/User.php 根本不会被扫描注册。
中间件按版本分组绑定
v1 用 session 鉴权,v2 强制 JWT,就不能把 JwtAuth::class 全局注册——TP 不会根据 /api/v2/ 这种路径字符串自动匹配中间件,它只认你在 Route::group() 链式调用里明确挂载的。
Route::group('api/v1')->middleware(['cors', 'session_auth']);
Route::group('api/v2')->middleware(['cors', 'jwt', 'rate_limit']);
别在 middleware.php 里写 ['api/v1' => [JwtAuth::class]] ——这行配置 TP 完全不识别,纯属无效代码。
服务层隔离代替控制器 if-else
控制器里写 if ($this->request->version === 'v2') { ... } 是自埋雷。下次加 v3 就得再嵌一层,很快变成判断黑洞。
方法一:用工厂模式加载对应服务类
在 v2.User 控制器的 read 方法里写:
$version = 'v2';
$serviceClass = "app\service\{$version}\UserService";
if (!class_exists($serviceClass)) { throw new HttpException(400, 'Unsupported API version'); }
$service = new $serviceClass();
return $service->getUser($id);
方法二:定义统一接口,v1/v2 分别实现
interface UserServiceInterface { public function getUser(int $id): array; }
app/service/v1/UserService 实现该接口,返回字段精简;
app/service/v2/UserService 同样实现,但额外注入新字段和校验逻辑。



















