正确做法是将版本号作为路由规则的静态前缀并显式声明,如'v1/' => '/',关闭enableStrictParsing;模块ID避免用v1/v2而用apiV1/apiV2;控制器按版本分目录隔离,通过命名空间和路由绑定确保分流;共用模型需通过契约接口实现;中间件应在路由解析后按路径前缀判断版本执行;数据库迁移、配置、测试均须严格隔离版本边界。

API 版本路由怎么配才不冲突
Yii 2 和 Yii 3 的路由机制都支持路径前缀匹配,但直接写 v1/ 和 v2/ 容易和模块 ID 或控制器命名撞车。比如你建了 modules/v1/ 目录,又在 urlManager 里配了 'v1/<controller:>/<action:>'</action:></controller:>,结果请求 /v1/user/view 时,框架可能优先去找 v1 模块而不是走路由规则——尤其当 v1 真是个模块时。
正确做法是把版本号作为路由规则的「静态前缀」,并确保它不被识别为模块别名:
- 在
urlManager.rules中显式声明:'v1/<controller:>/<action:>' => '<controller>/<action>'</action></controller></action:></controller:>,同时关闭enableStrictParsing(否则没匹配上的请求会 404) - 如果用模块承载版本逻辑,模块 ID 别叫
v1,改用apiV1、apiV2这类非纯数字 ID,避免和路由前缀语义重叠 - Yii 3 的
UrlRule支持pattern+route,更推荐用['pattern' => 'v2/<controller:>', 'route' => 'api-v2/<controller>']</controller></controller:>这种方式,把版本和模块彻底解耦
控制器怎么隔离 v1 和 v2 的逻辑
不能让 UserController 同时处理两个版本的字段校验、响应格式、权限逻辑——混在一起后期根本没法维护。最轻量也最可控的方式是按版本分控制器目录,但要注意命名空间和自动加载配置。
常见错误是只改了类名(如 UserV1Controller / UserV2Controller),却没同步更新路由映射或模块配置,导致请求进错控制器;或者两个控制器继承同一个基类,结果基类里加了个新字段校验,v1 接口就意外失效。
- Yii 2:在各自模块的
controllers/下放UserController.php,靠模块命名空间隔离,比如app\modules\apiV1\controllers\UserController和app\modules\apiV2\controllers\UserController - Yii 3:用独立目录 + 显式命名空间,例如
src/Api/V1/Controllers/UserController,并在config/routes.php中绑定'v1/user' => \App\Api\V1\Controllers\UserController::class - 共用模型?可以,但必须用「契约接口」而非具体类。比如定义
ApiUserContract,v1 和 v2 各自实现,避免模型层变更牵连多个版本
如何让 v1 和 v2 共享中间件但互不影响
鉴权、日志、限流这些中间件肯定想复用,但 v2 可能要支持 JWT 而 v1 还在用 session token,硬塞进同一个中间件里就会出现 if-else 嵌套地狱。
关键不是“能不能复用”,而是“复用哪一层”。路由解析之后、控制器执行之前,才是中间件介入的合理位置;而版本判断必须在这个阶段完成,否则后续逻辑无法分流。
- Yii 2:在
beforeAction()里读取$this->route,提取前缀判断版本,再调用对应校验方法,不要在全局bootstrap里做版本相关逻辑 - Yii 3:用
MiddlewareInterface实现版本感知中间件,构造函数注入Request,通过$request->getPathInfo()提取/v1/或/v2/,再决定是否执行 token 解析 - 别把版本判断逻辑藏在模型或服务层——那会让调试变得极其困难,一个 401 错误得翻遍三层代码才能定位是哪个版本的 token 校验挂了
升级 v2 时怎么保证 v1 不被意外改坏
最常踩的坑是改完 v2 的数据库迁移脚本,顺手把 v1 用的视图或存储过程也删了;或者在公共 config 里加了个 v2 专属的 responseFormatter,结果 v1 的 JSON 字段顺序全乱了。
版本共存的本质是「部署隔离」,不是「代码混写」。哪怕物理上放在同一个项目里,也要当成两个独立服务来约束边界。
- 数据库层面:v1 和 v2 的 migration 文件严格分开目录(如
migrations/v1/和migrations/v2/),上线前人工核对yii migrate --migrationPath=@app/migrations/v1是否真的只执行 v1 的变更 - 配置层面:用
Yii::getVersion()或环境变量区分加载逻辑,比如if (getenv('API_VERSION') === 'v2') { $config['response']['formatters']['json'] = ...; } - 测试层面:CI 流水线必须并行跑两套 API 测试,且 v1 的测试用例禁止访问任何带
v2/字样的 URL,防止误测漏测
版本管理最难的不是技术实现,而是每次改一行代码时,脑子里得同时记住两套契约——v1 的字段不能少,v2 的字段不能多,而中间件的开关状态必须精确到路径级。稍一走神,线上就出问题。


















