没有不值得去解决的问题,也没有不值得去学习的技术!

在 Yii 2 Starter Kit 中数据库迁移的多租户实现

运行命令:.\yii migrate/create create_news_table、.\yii migrate/create create_news_table --tenantid=default,成功运行
1、前文:https://www.shuijingwanwq.com/2018/01/18/2328/ 2、参考第11步骤,db 组件移至开发环境,以方便于 Gii 的使用,\common\config\base.php,如图1
参考第11步骤,db 组件移至开发环境,以方便于 Gii 的使用,\common\config\base.php
图1
3、配置为生产环境,以便于测试数据库迁移的多租户实现,不依赖于 db 组件,如图2
配置为生产环境,以便于测试数据库迁移的多租户实现,不依赖于 db 组件
图2
4、运行命令:.\yii app/setup,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图3
运行命令:.\yii app/setup,报错:Exception 'yii\base\InvalidConfigException' with message 'Failed to instantiate component or class "db".'
图3
5、基于 Trait 实现,将 getTenantDb() 方法放入 Trait 中,新建 \common\traits\TenantDb.php,通过 [[yii\di\ServiceLocator::setComponents()]] 方法注册数据库连接组件、RBAC组件
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/01/26
 * Time: 16:25
 */

namespace common\traits;

use Yii;
use common\logics\http\tenant\Env;
use yii\web\ServerErrorHttpException;

/**
 * 获取租户模块环境配置信息,存储至Redis,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

trait TenantDb
{
    /**
     * 数据库连接组件ID(基于多租户)
     *
     * @return string
     */
    public static function getTenantDb()
    {
        $env = new Env();
        $tenantEnv = $env->getTenantEnv();

        if ($tenantEnv === false) {
            if ($env->hasErrors()) {
                foreach ($env->getFirstErrors() as $message) {
                    $firstErrors = $message;
                }
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20006'), ['firstErrors' => $firstErrors])), 20006);
            } elseif (!$env->hasErrors()) {
                throw new ServerErrorHttpException('Multi-tenant HTTP requests fail for unknown reasons.');
            }
        }

        $tenantDb = $tenantEnv['data']['tenantid'] . 'Db';

        // 检查数据库连接组件、RBAC组件是否被注册
        if (!(Yii::$app->has($tenantDb) && Yii::$app->has('authManager'))) {
            // 注册数据库连接组件、RBAC组件
            Yii::$app->setComponents([
                $tenantDb => [
                    'class' => 'yii\db\Connection',
                    'dsn' => 'mysql:host=' . $tenantEnv['data']['db_info']['host'] . ';port=3306;dbname=' . $tenantEnv['data']['db_info']['database'] . '',
                    'username' => $tenantEnv['data']['db_info']['login'],
                    'password' => $tenantEnv['data']['db_info']['password'],
                    'tablePrefix' => $tenantEnv['data']['db_info']['prefix'],
                    'charset' => 'utf8',
                    'enableSchemaCache' => YII_ENV_PROD,
                    'schemaCache' => 'redisCache',
                ],
                'authManager' => [
                    'class' => 'yii\rbac\DbManager',
                    'db' => $tenantDb,
                    'itemTable' => '{{%rbac_auth_item}}',
                    'itemChildTable' => '{{%rbac_auth_item_child}}',
                    'assignmentTable' => '{{%rbac_auth_assignment}}',
                    'ruleTable' => '{{%rbac_auth_rule}}'
                ],
            ]);
        }

        return $tenantDb;
    }
}

</pre>
6、编辑 \common\components\db\ActiveRecord.php,导入 common\traits\TenantDb
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/16
 * Time: 10:31
 */

namespace common\components\db;

use Yii;
use common\traits\TenantDb;

/**
 * 导入 TenantDb,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class ActiveRecord extends \yii\db\ActiveRecord
{
    use TenantDb;

    /**
     * Returns the database connection used by this AR class.
     * By default, the "db" application component is used as the database connection.
     * You may override this method if you want to use a different database connection.
     * @return Connection the database connection used by this AR class.
     */
    public static function getDb()
    {
        $tenantDb = self::getTenantDb();
        return Yii::$app->$tenantDb;
    }
}

</pre>
7、基于 Trait 实现,导入 common\traits\TenantDb,将 init() 方法放入 Trait 中,新建 \common\traits\TenantMigration.php,将 $tenantDb 设置为数据库连接组件
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: WangQiang
 * Date: 2018/01/26
 * Time: 16:25
 */

namespace common\traits;

use Yii;
use common\traits\TenantDb;

/**
 * 导入 TenantDb,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

trait TenantMigration
{
    use TenantDb;

    /**
     * Initializes the migration.
     * This method will set [[db]] to be the 'db' application component, if it is `null`.
     */
    public function init()
    {
        $tenantDb = self::getTenantDb();
        $this->db = $tenantDb;
        parent::init();
    }
}

</pre>
8、新建 \common\components\db\Migration.php,导入 common\traits\TenantMigration
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/26
 * Time: 13:48
 */

namespace common\components\db;

use common\traits\TenantMigration;

/**
 * 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class Migration extends \yii\db\Migration
{
    use TenantMigration;
}

</pre>
9、在目录 \common\migrations 中查找:yii\db\Migration,批量替换为:common\components\db\Migration,如图4
在目录 \common\migrations 中查找:yii\db\Migration,批量替换为:common\components\db\Migration
图4
10、编辑 \common\migrations\db\m140703_123055_log.php,导入 common\traits\TenantMigration
<pre class="wp-block-syntaxhighlighter-code">

<?php
require(Yii::getAlias('@yii/log/migrations/m141106_185632_log_init.php'));

use common\traits\TenantMigration;

class m140703_123055_log extends m141106_185632_log_init
{
    use TenantMigration;
}


</pre>
11、编辑 \common\migrations\db\m140703_123813_rbac.php,导入 common\traits\TenantMigration
<pre class="wp-block-syntaxhighlighter-code">

<?php
require(Yii::getAlias('@yii/rbac/migrations/m140506_102106_rbac_init.php'));

use common\traits\TenantMigration;

class m140703_123813_rbac extends m140506_102106_rbac_init
{
    use TenantMigration;
}


</pre>
12、新建数据库迁移类,\console\controllers\MigrateController.php
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/26
 * Time: 17:35
 */

namespace console\controllers;

use common\traits\TenantMigration;

/**
 * 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class MigrateController extends \yii\console\controllers\MigrateController
{
    use TenantMigration;
}

</pre>
13、调整数据库迁移配置,编辑 \console\config\console.php,如图5


        'migrate' => [
            'class' => 'console\controllers\MigrateController',
            'migrationPath' => '@common/migrations/db',
            'migrationTable' => '{{%system_db_migration}}'
        ],


14、运行命令:.\yii app/setup,报错:Exception ‘yii\base\UnknownMethodException’ with message ‘Calling unknown method: yii\console\Request::get()’,如图6
运行命令:.\yii app/setup,报错:Exception 'yii\base\UnknownMethodException' with message 'Calling unknown method: yii\console\Request::get()'
图6
15、获取请求参数时,判断当前请求是否通过命令行进行,编辑 \common\logics\http\tenant\Env.php
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/17
 * Time: 15:20
 */

namespace common\logics\http\tenant;

use Yii;
use yii\base\Model;
use yii\web\BadRequestHttpException;
use yii\web\ServerErrorHttpException;

/**
 * 多租户的模块环境配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Env extends Model
{
    public $app_name;
    public $secret;
    public $tenant_id;

    public function attributeLabels()
    {
        return [
            'app_name' => \Yii::t('model/http/tenant/env', 'App Name'),
            'secret' => \Yii::t('model/http/tenant/env', 'Secret'),
            'tenant_id' => \Yii::t('model/http/tenant/env', 'Tenant ID'),
        ];
    }

    /**
     * 返回租户模块环境配置信息
     *
     * @return array|false
     *
     * 格式如下:
     *
     * 租户模块环境配置信息
     * [
     *     'message' => '', //说明
     *     'data' => [], //数据
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function getTenantEnv()
    {
        /* 获取请求参数 */
        $request = Yii::$app->request;

        // 判断当前请求是否通过命令行进行
        if ($request->isConsoleRequest) {
            $get = $request->getParams();

            /* 判断请求参数中租户ID是否存在 */
            if (empty($get[1])) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $get[1] = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $get[1];
        } else {
            $get = $request->get();

            /* 判断请求参数中租户ID是否存在 */
            if (empty($get['tenantid'])) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $get['tenantid'] = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $get['tenantid'];
        }

        // 设置多租户数据的缓存键
        $redisCache = Yii::$app->redisCache;
        $tenantKey = 'tenant:' . $this->tenant_id;

        // 从缓存中取回多租户数据
        $tenantData = $redisCache[$tenantKey];

        if ($tenantData === false) {
            $this->app_name = env('TENANT_APP_NAME');
            $this->secret = env('TENANT_SECRET');

            $response = Yii::$app->tenantHttp->createRequest()
                ->setMethod('get')
                ->setUrl('getTenantEnv')
                ->setData([
                    'appname' => $this->app_name,
                    'secret' => $this->secret,
                    'tenantid' => $this->tenant_id,
                ])
                ->send();
            // 检查响应状态码是否等于20x
            if ($response->isOk) {
                // 检查业务逻辑是否成功
                if ($response->data['returnCode'] === 0) {
                    $tenantData = ['message' => $response->data['returnDesc'], 'data' => $response->data['returnData']];
                    // 将多租户数据存放到缓存供下次使用
                    $redisCache[$tenantKey] = $tenantData;
                    return $tenantData;
                } else {
                    $this->addError('tenant_id', $response->data['returnDesc']);
                    return false;
                }
            } else {
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20005'), ['statusCode' => $response->getStatusCode()])), 20005);
            }
        } else {
            return $tenantData;
        }

    }

}

</pre>
16、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception ‘yii\db\Exception’ with message ‘SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘cmcp-api.ca_system _db_migration’ doesn’t exist’,如图7
删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception 'yii\db\Exception' with message 'SQLSTATE[42S02]: Base table or view not found: 1146 Table 'cmcp-api.ca_system _db_migration' doesn't exist'
图7
17、运行命令:.\yii app/setup,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘You should configure “log” component to use one or more databa se targets before executing this migration.’,如图8
运行命令:.\yii app/setup,报错:Exception 'yii\base\InvalidConfigException' with message 'You should configure "log" component to use one or more databa se targets before executing this migration.'
图8
18、删除 E:\wwwroot\cmcp-api\common\migrations\db\m140703_123055_log.php,因为日志组件已经配置为基于文件存储了的。 19、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception: Undefined class constant ‘STATUS_PUBLISHED’ (E:\wwwroot\cmcp-api\common\migrations\db\m150725_192740_seed_dat a.php:63),如图9
删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception: Undefined class constant 'STATUS_PUBLISHED' (E:\wwwroot\cmcp-api\common\migrations\db\m150725_192740_seed_dat a.php:63)
图9
20、编辑 \common\migrations\db\m150725_192740_seed_data.php,\common\models\Page 替换为 \common\logics\Page,如图10


        $this->insert('{{%page}}', [
            'slug' => 'about',
            'title' => 'About',
            'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
            'status' => \common\logics\Page::STATUS_PUBLISHED,
            'created_at' => time(),
            'updated_at' => time(),
        ]);


编辑 \common\migrations\db\m150725_192740_seed_data.php,\common\models\Page 替换为 \common\logics\Page
图10
21、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception ‘yii\base\UnknownPropertyException’ with message ‘Getting unknown property: yii\console\Application::tenantHtt p’,如图11
删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception 'yii\base\UnknownPropertyException' with message 'Getting unknown property: yii\console\Application::tenantHtt p'
图11
22、通过应用组件配置客户端,编辑 \common\config\web.php,将 tenantHttp 组件移至 \common\config\base.php,如图12


        'tenantHttp' => [
            'class' => 'yii\httpclient\Client',
            'baseUrl' => Yii::getAlias('@tenantUrl'),
            'transport' => 'yii\httpclient\CurlTransport'
        ],


通过应用组件配置客户端,编辑 \common\config\web.php,将 tenantHttp 组件移至 \common\config\base.php
图12
23、删除数据库中所有表,运行命令:.\yii app/setup default,Migrated up successfully.如图13
删除数据库中所有表,运行命令:.\yii app/setup default,Migrated up successfully
图13
24、删除数据库中所有表,运行命令:.\yii migrate default,Migrated up successfully. 25、继续运行命令:.\yii rbac-migrate default,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图14
继续运行命令:.\yii rbac-migrate default,报错:Exception 'yii\base\InvalidConfigException' with message 'Failed to instantiate component or class "db".'
图14
26、编辑 RBAC 数据库迁移类,\console\controllers\RbacMigrateController.php,
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace console\controllers;

use yii\console\controllers\MigrateController;
use common\traits\TenantMigration;

/**
 * @author Eugene Terentev <eugene@terentev.net>
 */
class RbacMigrateController extends MigrateController
{
    use TenantMigration;

    /**
     * Creates a new migration instance.
     * @param string $class the migration class name
     * @return \common\rbac\Migration the migration instance
     */
    protected function createMigration($class)
    {
        $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
        require_once($file);

        return new $class();
    }
}


</pre>
27、继续运行命令:.\yii rbac-migrate default,Migrated up successfully.如图15
继续运行命令:.\yii rbac-migrate default,Migrated up successfully.
图15
28、打开网址:http://backend.cmcp-api.localhost/ ,报错:SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘cmcp-api.ca_system_log’ doesn’t exist The SQL being executed was: SELECT COUNT(*) FROM `ca_system_log`,如图16
打开网址:http://backend.cmcp-api.localhost/ ,报错:SQLSTATE[42S02]: Base table or view not found: 1146 Table 'cmcp-api.ca_system_log' doesn't exist The SQL being executed was: SELECT COUNT(*) FROM `ca_system_log`
图16
29、编辑 \backend\views\layouts\common.php,涉及到 SystemLog:: 的相关代码,需要删除掉,因为在实现数据库迁移时,系统日志表文件已经删除。 30、运行命令:.\yii migrate/create create_news_table,报错:Exception ‘yii\web\ServerErrorHttpException’ with message ‘多租户HTTP请求失败:模块信息未配置’,如图17
运行命令:.\yii migrate/create create_news_table,报错:Exception 'yii\web\ServerErrorHttpException' with message '多租户HTTP请求失败:模块信息未配置'
图17
31、决定将租户ID从参数形式转换为选项,如–tenantid=default,编辑 \common\logics\http\tenant\Env.php


        // 判断当前请求是否通过命令行进行
        if ($request->isConsoleRequest) {
            $get = $request->getParams();

            foreach ($get as $value) {
                $option = explode('=', $value);
                if ($option[0] == '--tenantid') {
                    $optionValue = $option[1];
                    break;
                }
            }

            /* 判断请求参数中租户ID是否存在 */
            if (empty($optionValue)) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $optionValue = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $optionValue;
        } else {
            $get = $request->get();

            /* 判断请求参数中租户ID是否存在 */
            if (empty($get['tenantid'])) {
                // throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
                $get['tenantid'] = env('TENANT_DEFAULT_ID');
            }
            $this->tenant_id = $get['tenantid'];
        }


32、通过覆盖在 [[yii\console\Controller::options()]] 中的方法, 以指定可用于控制台命令(controller/actionID)选项。编辑 \console\controllers\AppController.php
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace console\controllers;

use Yii;
use yii\console\Controller;
use yii\helpers\Console;

/**
 * @author Eugene Terentev <eugene@terentev.net>
 */
class AppController extends Controller
{
    public $writablePaths = [
        '@common/runtime',
        '@frontend/runtime',
        '@frontend/web/assets',
        '@backend/runtime',
        '@backend/web/assets',
        '@api/runtime',
        '@api/web/assets',
        '@storage/cache',
        '@storage/web/source'
    ];

    public $executablePaths = [
        '@backend/yii',
        '@api/yii',
        '@frontend/yii',
        '@console/yii',
    ];

    public $generateKeysPaths = [
        '@base/.env'
    ];

    public $tenantid;

    public function options($actionID)
    {
        return ['color', 'interactive', 'help', 'tenantid'];
    }

    public function actionSetup()
    {
        $this->runAction('set-writable', ['interactive' => $this->interactive]);
        $this->runAction('set-executable', ['interactive' => $this->interactive]);
        $this->runAction('set-keys', ['interactive' => $this->interactive]);
        \Yii::$app->runAction('migrate/up', ['interactive' => $this->interactive]);
        \Yii::$app->runAction('rbac-migrate/up', ['interactive' => $this->interactive]);
    }

    public function actionSetWritable()
    {
        $this->setWritable($this->writablePaths);
    }

    public function actionSetExecutable()
    {
        $this->setExecutable($this->executablePaths);
    }

    public function actionSetKeys()
    {
        $this->setKeys($this->generateKeysPaths);
    }

    public function setWritable($paths)
    {
        foreach ($paths as $writable) {
            $writable = Yii::getAlias($writable);
            Console::output("Setting writable: {$writable}");
            @chmod($writable, 0777);
        }
    }

    public function setExecutable($paths)
    {
        foreach ($paths as $executable) {
            $executable = Yii::getAlias($executable);
            Console::output("Setting executable: {$executable}");
            @chmod($executable, 0755);
        }
    }

    public function setKeys($paths)
    {
        foreach ($paths as $file) {
            $file = Yii::getAlias($file);
            Console::output("Generating keys in {$file}");
            $content = file_get_contents($file);
            $content = preg_replace_callback('/<generated_key>/', function () {
                $length = 32;
                $bytes = openssl_random_pseudo_bytes(32, $cryptoStrong);
                return strtr(substr(base64_encode($bytes), 0, $length), '+/', '_-');
            }, $content);
            file_put_contents($file, $content);
        }
    }
}


</pre>
33、通过覆盖在 [[yii\console\Controller::options()]] 中的方法, 以指定可用于控制台命令(controller/actionID)选项。编辑 \console\controllers\MigrateController.php、\console\controllers\RbacMigrateController.php \console\controllers\MigrateController.php
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/26
 * Time: 17:35
 */

namespace console\controllers;

use common\traits\TenantMigration;

/**
 * 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class MigrateController extends \yii\console\controllers\MigrateController
{
    use TenantMigration;

    public $tenantid;

    public function options($actionID)
    {
        return ['color', 'interactive', 'help', 'tenantid'];
    }
}

</pre>
\console\controllers\RbacMigrateController.php
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace console\controllers;

use yii\console\controllers\MigrateController;
use common\traits\TenantMigration;

/**
 * @author Eugene Terentev <eugene@terentev.net>
 */
class RbacMigrateController extends MigrateController
{
    use TenantMigration;

    public $tenantid;

    public function options($actionID)
    {
        return ['color', 'interactive', 'help', 'tenantid'];
    }

    /**
     * Creates a new migration instance.
     * @param string $class the migration class name
     * @return \common\rbac\Migration the migration instance
     */
    protected function createMigration($class)
    {
        $file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
        require_once($file);

        return new $class();
    }
}


</pre>
34、删除数据库中所有表,调整第24、25、26步骤的命令,.\yii app/setup –tenantid=default、.\yii migrate –tenantid=default、.\yii rbac-migrate –tenantid=default,成功运行 35、运行命令:.\yii migrate/create create_news_table、.\yii migrate/create create_news_table –tenantid=default,成功运行,如图18
运行命令:.\yii migrate/create create_news_table、.\yii migrate/create create_news_table --tenantid=default,成功运行
图18
36、由于已经在控制器中定义了数据库 application component 的 ID,将第8(删除 \common\components\db\Migration.php)、9、11步骤还原,在目录 \common\migrations 中查找:common\components\db\Migration,批量替换为:yii\db\Migration,如图19
由于已经在控制器中定义了数据库 application component 的 ID,将第8(删除 \common\components\db\Migration.php)、9、11步骤还原,在目录 \common\migrations 中查找:common\components\db\Migration,批量替换为:yii\db\Migration
图19
 

PHP / Laravel / Yii2 老项目维护与长期技术支持

如果你的 PHP / Laravel / Yii2 项目已经上线,但遇到原开发离职、Bug 长期无人修复、接口不稳定、性能下降、代码难以接手等问题,可以联系我做一次远程技术排查。

适合以下情况:
✅ 老旧 PHP 系统无人维护
✅ Laravel / Yii2 项目 Bug 修复
✅ 后台管理系统小功能迭代
✅ RESTful API 接口排查
✅ MySQL / Redis / Nginx 性能问题
✅ 长期远程兼职维护

可先从一次小问题开始:
✅ 线上报错排查
✅ 接口异常分析
✅ 慢查询与性能瓶颈定位
✅ 代码结构初步评估
✅ 部署环境与日志检查

如需咨询,请联系我,并注明:PHP 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理