最稳方案是用 Route::group() 配静态前缀和小写命名空间目录;动态版本参数会导致缓存失效404;目录名、命名空间、类名须严格小写且一致;v1/v2 中间件需分别绑定;禁止控制器内 if-else 版本分流;改路由后须手动清缓存。

直接结论:用 Route::group() 配静态前缀 + 独立命名空间目录,是最稳、最易调试、最兼容 ThinkPHP 路由缓存的方案。别碰动态 :version 参数路由,除非你真想半夜修 404。
Route::group('v1') 必须写死字符串,不能拼接或读配置
ThinkPHP 的路由缓存机制在构建时会把 Route::group() 的第一个参数当作字面量硬编码进缓存文件。如果你写成 Route::group(config('api.version.v1'), ...) 或 Route::group($version, ...),开发环境可能跑得通(因为没开缓存),但上线后缓存生成失败,所有该组路由全 404。
- ✅ 正确写法:
Route::group('v1', function () { Route::get('user/:id', 'api/v1.User/read'); }); - ❌ 错误写法:
$v = 'v1'; Route::group($v, ...)、Route::group(env('API_V1_PREFIX'), ...) - 注意:前缀不带斜杠,
'v1'就行,不是'/v1',TP 会自动拼接
控制器命名空间和目录名必须严格小写且对齐
Linux 服务器区分大小写,app/api/V1/User.php 和 app/api/v1/User.php 是两个路径。哪怕你在 Windows 写代码没问题,部署到生产环境立刻报 Class app\api\v1\User does not exist——其实类存在,只是 PSR-4 自动加载器按小写路径去找,根本找不到大写的 V1 目录。
- 目录结构必须是:
app/api/v1/User.php、app/api/v2/User.php - 对应命名空间必须是:
namespace app\api\v1;、namespace app\api\v2; - 类名必须与文件名一致:
User.php→class User,不是UserController(除非你在路由里显式写api/v1.UserController/read)
v1 和 v2 路由中间件要分开绑定,别共用一个 middleware.php 配置
ThinkPHP 不会根据 URL 路径字符串(比如 /v1/)自动匹配中间件。它只认你在 Route::group() 链式调用中明确指定的中间件,或者路由规则名(rule name)绑定的中间件。
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
立即学习“PHP免费学习笔记(深入)”;
- ✅ 正确绑定:
Route::group('v1', ...)->middleware(['cors', 'throttle:60,1']); - ✅ v2 单独配:
Route::group('v2', ...)->middleware(['cors', 'throttle:100,1', 'jwt']); - ❌ 无效写法:
['api/v1' => [JwtAuth::class]]放在middleware.php里——TP 根本不解析这个路径字符串 - 如果 v2 新增了 JWT 鉴权,而 v1 还是 session,就绝不能全局注册
JwtAuth::class,否则 v1 请求直接 401
别在控制器里手动判断版本号再 if-else 分流
拿到 request()->param('version') 后写 if ($v === 'v1') { ... } else { ... },短期看着快,三个月后这个方法会膨胀成 200 行嵌套判断,且无法单元测试、无法复用、无法单独压测 v2 逻辑。
- ✅ 推荐做法:每个版本用独立控制器,业务逻辑下沉到 service 层,例如:
app/service/v1/UserService.php和app/service/v2/UserService.php实现同一接口 - ✅ 控制器只做薄层转发:
$service = new $serviceClass(); return $service->get($id); - ⚠️ 注意:v2 的 DTO 转换、字段映射(如
user_name→name)、默认值策略(input('page/d', 1))都得在各自控制器里显式声明,别指望继承父类就自动覆盖
真正容易被忽略的点是:路由缓存开启后,改完 v2 的路由或控制器,必须手动运行 php think route:clear;否则旧缓存还在,你改了十遍 app/api/v2/User.php,请求还是走的 v1 的老逻辑。


















