Posted on

做一个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


然后,注册 tenant包的 服务提供者,service provider inconfig/app.php.

一般来说,会继承原来的Tenant model, 进一步修改,同时也要在config/tenancy.php 配置好 Model:

'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 修改路由,这样就可以进入管理中心的路由,而不是进入租户的路由:

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是识别租户。

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 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)
租户的应用前端:

租户后台:

管理中心后台:


同时,可以在代码里面对租户进行如下操作:

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 => [],
·····

例如,更新数据表内容如下:

例如,生成租户文件夹如下:



** 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