💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 做一个SaaS独立站(2)- 安装配置 参考: https://tenancyforlaravel.com/docs/v3/quickstart/ 一步一步来: 整个流程大概如下: > 0,配置好租户的事件和数据建表,生成租户,触发各种bootstrap初始化。 > 1,租户域名--》识别租户--》切换租户数据库--》切换各种资源--》运行应用--》运行相应任务命令 > 2,主域名--》识别管理中心--》切换主数据库--》运行管理后台--》管理租户 ### 第一:先安装laravel-shop 源码:https://github.com/summerblue/laravel-shop/tree/L05_8.x 安装后,简单运行一下是否正常,然后我们接下来把它改造成SaaS. ### 第二:安装 archtechx/tenancy 源码: https://github.com/archtechx/tenancy ``` composer require stancl/tenancy ``` ``` php artisan tenancy:install ``` 安装后生成:migrations, config file, route file and a service provider 然后 ,执行数据库迁移,生成 tenants 租户表,domains 域名表: ``` php artisan migrate ``` ![](https://img.kancloud.cn/b1/4f/b14fcbe2c4054738dde202ec58da303a_393x59.png) 然后,注册 tenant包的 服务提供者,service provider in`config/app.php`. ![](https://img.kancloud.cn/b1/ee/b1ee99fe715472d47c4e7e59b0e715bb_673x323.png) 一般来说,会继承原来的Tenant model, 进一步修改,同时也要在`config/tenancy.php` 配置好 Model: ![](https://img.kancloud.cn/63/46/6346b0f9a9f3f4816c2133ea7dd27acc_709x448.png) ``` 'tenant\_model' => \App\Models\Tenant::class, ``` 这样!就算安装好了!下一步,我们要配置好 SaaS多租户的功能。 ***** ***** ## 第三,配置租户生成事件(Events): 当新建租户的时候,会触发事件任务,例如执行 生成数据库CreateDatabase,迁移数据migration,填充数据seeder等等,在文件`TenancyServiceProvider`这里是配置 任务: ``` public function events() { return [ // Tenant events Events\CreatingTenant::class => [], Events\TenantCreated::class => [ JobPipeline::make([ Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, //Jobs\SeedDatabase::class, CreateFrameworkDirectoriesForTenant::class, //建立租户文件夹 UpdateAdminMenuForTenant::class //更新租户数据表 // Your own jobs to prepare the tenant. // Provision API keys, create S3 buckets, anything you want! ])->send(function (Events\TenantCreated $event) { return $event->tenant; })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. ], ``` ## 第四,配置管理中心路由(Central routes): 在`app/Providers/RouteServiceProvider.php` 修改路由,这样就可以进入管理中心的路由,而不是进入租户的路由: ~~~php public function boot() { $this->configureRateLimiting(); $this->mapWebRoutes(); $this->mapApiRoutes(); } protected function mapWebRoutes() { foreach ($this->centralDomains() as $domain) { Route::middleware('web') ->domain($domain) ->namespace($this->namespace) ->group(base_path('routes/web.php')); } } protected function mapApiRoutes() { foreach ($this->centralDomains() as $domain) { Route::prefix('api') ->domain($domain) ->middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); } } protected function centralDomains(): array { return config('tenancy.central_domains'); } ~~~ ## 第五,配置租户路由(Central routes): 在` routes/tenant.php` 配置租户的路由, `PreventAccessFromCentralDomains`的Middleware中间件是过滤掉不准主域名进入。`InitializeTenancyByDomain`是识别租户。 ~~~php Route::middleware([ 'web', InitializeTenancyByDomain::class, PreventAccessFromCentralDomains::class, ])->group(function () { Route::get('/', function () { return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id'); }); }); ~~~ ## 第六,配置数据迁移Migrations 手动把` database/migrations` 相关需要迁移的文件 复制到 `database/migrations/tenant` 里面。当执行租户数据迁移时候就会自动执行,生成租户需要的数据表: ``` php artisan tenants:migrate ``` ## 最后,生成租户测试: Tenant 是生成租户,而Domain是绑定租户Tenant,访问域名进行识别租户: ~~~php $ php artisan tinker >>> $tenant1 = App\Models\Tenant::create(['id' => 'foo']); >>> $tenant1->domains()->create(['domain' => 'foo.localhost']); >>> >>> $tenant2 = App\Models\Tenant::create(['id' => 'bar']); >>> $tenant2->domains()->create(['domain' => 'bar.localhost']); ~~~ 这时候,你可以浏览器访问 'foo.localhost' ,就能够进入 租户的应用前端了。 (注:要在hosts绑定域名和本地ip) 租户的应用前端: ![](https://img.kancloud.cn/ab/cd/abcd13b04f14402ba34d1ddc6d10f5ff_755x721.png) 租户后台: ![](https://img.kancloud.cn/18/12/18123256ff62e54ae6901c8ff4abdacd_1169x438.png) 管理中心后台: ![](https://img.kancloud.cn/af/7f/af7fa1de994dd701244eae76a9f4aac1_1309x481.png) ***** 同时,可以在代码里面对租户进行如下操作: ~~~php App\Models\Tenant::all()->runForEach(function () { App\Models\User::factory()->create(); // 切换租户,执行操作 }); ~~~ 完成!以上就是多租户SaaS的基本安装和配置。下面具体说说配置的知识点。 ***** ***** ## 注:配置的知识点: #### 1,Config/tenancy.php 配置 ``` 'tenant_model' => \App\Models\Tenant::class, //配置好 Tenant和Domain的class ``` ``` 'central_domains' => [ str_replace(['https//', 'http//'], '', env('APP_URL')), ], // 配置好 管理中心的URL ``` ``` //租户识别后,启动资源隔离: 'bootstrappers' => [ Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed ], ``` ***** #### 2, TenancyServiceProvider.php 配置: ** 2.1 Events\TenantCreated (租户生成时)配置:** 租户生成时,具体执行的生成任务配置,这里举例几个: ``` ···· public function events() { return [ // Tenant events Events\CreatingTenant::class => [], Events\TenantCreated::class => [ JobPipeline::make([ Jobs\CreateDatabase::class, //生成数据库 Jobs\MigrateDatabase::class, //迁移数据表 //Jobs\SeedDatabase::class, //填充数据 CreateFrameworkDirectoriesForTenant::class, //生成租户文件夹 UpdateAdminMenuForTenant::class //更新租户数据内容 // Your own jobs to prepare the tenant. // Provision API keys, create S3 buckets, anything you want! ])->send(function (Events\TenantCreated $event) { return $event->tenant; })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. ], Events\SavingTenant::class => [], ····· ``` 例如,更新数据表内容如下: ![](https://img.kancloud.cn/99/07/99078b27c674005bd8595a8053ac1cc9_717x582.png) 例如,生成租户文件夹如下: ![](https://img.kancloud.cn/21/c6/21c60d389f703d93e120a257779e2e10_598x425.png) ***** ***** ** 2.2 boot() 启动租户任务配置:** ``` public function boot() { $this->bootEvents(); //启动事件监听 $this->mapRoutes(); //启动路由监听 $this->makeTenancyMiddlewareHighestPriority(); //以下是我们添加的 自定义配置 InitializeTenancyByDomain::$onFail = function () { return redirect(env('APP_URL')); //租户访问失败,跳转主访问 }; TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomainOrSubdomain::class; // 静态资源相关. // 租户自定义配置. // @see https://tenancyforlaravel.com/docs/v3/features/tenant-config TenantConfig::$storageToConfigMap = [ // Do whatever you want. ]; DomainTenantResolver::$shouldCache = true; //租户路由缓存配置 } protected function mapRoutes() { if (file_exists(base_path('routes/tenant.php'))) { Route::namespace(static::$controllerNamespace) ->group(base_path('routes/tenant.php')); } } ``` ***** ***** #### 3,migration 和 seeder 初始化数据 配置 在`config/tenancy.php` 可以配置相关参数,我的习惯是 不要seeder,直接把有需要的seeder做成 一个migration,直接执行migration。 ``` /** * Parameters used by the tenants:migrate command. */ 'migration_parameters' => [ '--force' => true, // This needs to be true to run migrations in production. '--path' => [database_path('migrations/tenant')], '--realpath' => true, ], /** * Parameters used by the tenants:seed command. */ 'seeder_parameters' => [ '--class' => 'DatabaseSeeder', //'TenantDatabaseSeeder', // root seeder class //'--force' => true, ], ``` #### 4,路由配置: 这里需要的路由配置有:中心应用路由,管理中心路由,租户应用路由,租户管理后台路由。 4.1 管理中心的路由配置 `app/Admin/routes.php`例子如下 : ``` /** * 超级管理员可以通过此路由进入租户后台. */ Route::group([ 'prefix' => config('admin.route.prefix'), 'namespace' => config('admin.route.namespace'), 'middleware' => config('admin.route.middleware'), 'domain' => config('tenancy.central_domains')[0], //限定管理中心域名才能进入 ], function (Router $router) { // 租户管理 $router->resource('/tenant', 'TenantController'); // 域名管理 $router->resource('/domain', 'DomainController')->only(['index', 'destroy', 'show']); $router->get('/', 'HomeController@index'); $router->get('users', 'UsersController@index'); $router->get('products', 'ProductsController@index'); }); ``` 4.2 租户管理中心的路由配置 `app/Admin/routes.php`例子如下 : ``` /** * 租户管理员可以通过此路由进入租户后台. */ Route::middleware([ 'web','admin', // 要经过管理员登录验证 CheckTenantForMaintenanceMode::class, //检查是否维护状态 ScopeSessions::class, InitializeTenancyByDomain::class, //识别租户,执行切换资源 PreventAccessFromCentralDomains::class, //防止管理中心访问的混入 ]) ->prefix(config('admin.route.prefix')) ->namespace(config('admin.route.namespace')) ->group(function (Router $router) { $router->get('/', 'HomeController@index'); $router->get('users', 'UsersController@index'); $router->get('products', 'ProductsController@index'); $router->get('products/create', 'ProductsController@create'); $router->post('products', 'ProductsController@store'); $router->get('products/{id}/edit', 'ProductsController@edit'); $router->put('products/{id}', 'ProductsController@update'); // 开启上帝模式,管理中心是可以直接访问租户后台 $router->get('/god/{token}', function ($token) { return UserImpersonation::makeResponse($token); }); }); ``` 4.3 中心应用路由的路由配置 `routes/web.php`例子如下 : ``` //就是普通平时的路由,不需要解释 Route::get('/', 'PagesController@root')->name('root'); Auth::routes(); // 在之前的路由里加上一个 verify 参数 Auth::routes(['verify' => true]); Route::get('products/favorites', 'ProductsController@favorites')->name('products.favorites'); // auth 中间件代表需要登录,verified中间件代表需要经过邮箱验证 Route::group(['middleware' => ['auth', 'verified']], function() { Route::get('user_addresses', 'UserAddressesController@index')->name('user_addresses.index'); ``` 4.4 租户应用的路由配置 `routes/tenant.php`例子如下 : ``` Route::middleware([ 'web', InitializeTenancyByDomain::class, //识别租户,切换资源 PreventAccessFromCentralDomains::class, //防止中心应用的访问混入 ])->group(function () { Route::get('/', 'PagesController@root')->name('root'); Auth::routes(); // 按正常的用户验证就可以 // 在之前的路由里加上一个 verify 参数 Auth::routes(['verify' => true]); Route::get('products/favorites', 'ProductsController@favorites')->name('products.favorites'); // auth 中间件代表需要登录,verified中间件代表需要经过邮箱验证 Route::group(['middleware' => ['auth', 'verified']], function() { Route::get('user_addresses', 'UserAddressesController@index')->name('user_addresses.index'); ····· ···· ``` ***** ### 路由知识点: 中心central 和 租户tenants 路由 的 相互限制方式: ``` 'domain' => config('tenancy.central_domains')[0], //限定管理中心域名才能进入 ``` ``` PreventAccessFromCentralDomains::class, //防止中心应用的访问混入 ``` ***** ## 租户基本命令: 1,租户命令: ``` php artisan tenants:migrate php artisan tenants:migrate --seed php artisan tenants:migrate-fresh --seed php artisan tenants:seed --tenants=XXXX php artisan tenants:run larabbs:calculate-active-user php artisan tenants:run email:send --tenants=8075a580-1cb8-11e9-8822-49c5d8f8ff23 --option="queue=1" --option="subject=New Feature" --argument="body=We have launched a new feature. ..." ``` 2,cron Kernel 配置租户执行命令方式: ``` $schedule->command('tenants:run larabbs:calculate-active-user')->everyMinute()->withoutOverlapping(); ``` ***** ## 多租户的图片资源使用方式: 默认是不对的地址: ``` http://foo9.larashop.test/storage/images/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87\_20211116190644.jpg ``` 正确的图片地址应该是这样,有 `tenancy/assets` : ``` http://foo9.larashop.test/tenancy/assets/images/%E5%BE%AE%E4%BF%A1%E5%9B%BE%E7%89%87\_20211116190644.jpg ``` 所以要图片资源的函数方法: ``` public function tenancyUrl($path) {         if (URL::isValidUrl($path)) {             return $path;         }         if (tenant()) {             return tenant_asset($path); // 这里是关键,会转换租户地址         }         return $this->getStorage()->url($path);     } ``` ***** ## 其他资源的隔离注意: 队列,redis, redis缓存 , 多租户, 文件独立 等都需要注意隔离的配置。 #### 例如:文件缓存的报错, 有些资源是需要用tenant-aware的,如配置利用redis。 > This cache store does not support tagging > Hi. If you want your cache to be tenant-aware, you need to use a driver that supports tagging, e.g. Redis. > If you don't need tenant-aware cache, comment out the CacheTenancyBootstrapper in your tenancy.php config file. ***** ### 代码的github地址: 为了方便参考,这里提供我的github地址,有相关代码参考:https://github.com/liangdabiao/laravel-shop-saas 所有账号密码都是 admin admin 后台 /admin ***** 同时也可以参考我的论坛SaaS代码,另一种方式:https://github.com/liangdabiao/bbs-saas-skeleton