三处改动并不落在同一个地方:加页面主要动 routes/ 与视图目录(Laravel 风格下是 resources/views,Inertia 项目是 resources/js/Pages);加表动 database/migrations 与 app/Models;换模型只动调用封装文件与它读取的配置,业务代码不碰框架核心。判断一条准则是:改动如果落在 vendor/、bootstrap/、框架自带中间件这类目录里,下次执行 composer update 或项目升级就会冲突或被覆盖,那就说明位置选错了。下面按三类改动分别给目录、最小骨架和自检方式,目录名以你项目实际结构为准,仅作对照。
跑通示例后再加功能,最稳的顺序是先标出业务目录,再按“页面归路由与视图、数据归迁移与模型、模型调用归封装”分工。三类改动各自独立提交,每完成一类就用route:list、migrate输出或一条请求的日志确认行为,出错时能单独撤销。边界是:凡需要改框架核心文件才能实现的,优先换成扩展点或事件,而不是直接改源码。
先标出示例项目里属于业务代码的目录
动手前先把目录树拆成两半。MkSaaS 这类脚手架通常把示例功能放在同样的位置,跟着示例走基本不会错:示例页面在 routes/web.php 里注册,控制器在 app/Http/Controllers,视图在 resources/views 或 resources/js/Pages,示例数据表在 database/migrations,对应模型在 app/Models,第三方调用一般收在 app/Services 或 app/Support,参数在 config/ 与 .env。
判断依据很直接:在示例功能里搜一个字符串,比如某个页面标题,看它出现在哪些文件;这些文件所在目录就是业务可改区。反过来,vendor/、bootstrap/cache、composer.lock,以及框架自带的 Kernel、异常处理、基础中间件,不建议改动——它们属于依赖与框架层,升级时会被整体替换,你改的内容会被覆盖或产生合并冲突。确需改变框架行为,先用中间件、事件监听、服务提供者这类扩展点包一层。
加一个页面并从路由注册走到渲染
页面由两件事组成:一条路由和它指向的视图。路由写在 routes/web.php 业务路由区,视图放在示例页面旁边,别新建顶层目录。
// routes/web.php 顶部按现有写法引入控制器
use App\Http\Controllers\ReportController;
Route::middleware(['auth'])->group(function () {
Route::get('/reports/monthly', [ReportController::class, 'monthly'])
->name('reports.monthly');
});// app/Http/Controllers/ReportController.php
public function monthly()
{
return view('reports.monthly', ['title' => '月度报表']);
}视图文件放 resources/views/reports/monthly.blade.php(Inertia 项目则放 resources/js/Pages/Reports/Monthly.vue 并在控制器返回 Inertia::render)。改完先跑 php artisan route:list `--path`=reports,确认路由已注册且中间件正确;再用已登录账号访问 /reports/monthly,预期看到新页面而不是 404 或 500。未登录访问应被重定向到登录页,这一步能验证中间件生效。
加一张表并生成迁移文件
表结构只走迁移,不手改数据库。生成时把模型和迁移一起建出来,避免文件名对不上:
php artisan make:model ReportSnapshot -m迁移写在 database/migrations/ 下新生成的时间戳文件,只改 up() 与 down():
Schema::create('report_snapshots', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->index();
$table->string('period', 20);
$table->json('payload')->nullable();
$table->timestamps();
});执行 php artisan migrate,输出里应出现这条迁移文件名。确认表结构有三种方式,任选其一:php artisan db:show `--table`=report_snapshots、进入数据库执行 describe report_snapshots、或 php artisan tinker 里 \App\Models\ReportSnapshot::query()->limit(1)->get() 能返回空集合而不报错。字段填充规则和索引放在模型 $fillable 与迁移里,别在控制器里拼 SQL。
在调用封装里替换一组参数
换模型指的是换某个模型的标识、地址或采样参数,动作应集中在一处。先找现有封装:多数脚手架会把调用收在类似 app/Services/Llm/Client.php 的类里,参数从 config/*.php 读取,最终来自 .env。
// config/llm.php
return [
'default' => env('LLM_PROVIDER', 'provider_a'),
'providers' => [
'provider_a' => [
'model' => env('LLM_MODEL', 'model-a'),
'timeout' => env('LLM_TIMEOUT', 30),
],
],
];换模型时优先改 .env 的 LLM_MODEL,封装类保持不动;只有在请求体结构不一致时,才在封装里按 provider 分支适配,业务代码仍只调用封装方法。改完用一条最小请求验证:触发一次使用该模型的页面或命令,看日志中记录的请求参数里 model 字段是否已是新值,以及返回是否成功解析。观察点在日志和响应结构,不在“感觉快了多少”。
逐类回滚一次,确认改动互不影响
回滚按“先数据后界面、先上层后底层”的顺序:先撤模型调用改动(把 .env 改回原值并清理配置缓存 php artisan config:clear),再撤表(php artisan migrate:rollback `--step`=1),再删视图文件,最后删路由。每步之后都要重新验证:
- 回滚模型调用:触发同一条请求,日志中 model 字段应为原值,页面表现回到改动前。
- 回滚迁移:
php artisan migrate:status中该迁移回到 Pending,describe report_snapshots报表不存在。 - 删除视图:访问
/reports/monthly应出现视图缺失错误,说明页面确实只依赖这个文件。 - 删除路由:
php artisan route:list `--path`=reports不再列出该路由,访问返回 404。
四步都单独执行过,再一起恢复,就能确认三类改动之间没有隐式依赖。建议每次只提交一类改动,提交信息写清“页面、表、模型调用”,出问题时能按提交粒度单独回退。