diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..a4affc8ed --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,22 @@ +parameters: + level: 3 + paths: + - app/ + - config/ + - plugin/ + excludePaths: + - */runtime/* + - */vendor/* + - */tests/* + scanFiles: + - vendor/topthink/framework/src/helper.php + - vendor/topthink/think-orm/src/helper.php + - vendor/topthink/think-validate/src/helper.php + - plugin/think-library/src/common.php + - plugin/think-plugs-system/src/common.php + - plugin/think-plugs-worker/src/common.php + - plugin/think-plugs-wemall/src/common.php + - phpstan/stubs/legacy-plugin-aliases.php + - phpstan/stubs/composer-plugin-runtime.php + bootstrapFiles: + - tests/bootstrap.php diff --git a/phpstan/stubs/composer-plugin-runtime.php b/phpstan/stubs/composer-plugin-runtime.php new file mode 100644 index 000000000..6fff6358e --- /dev/null +++ b/phpstan/stubs/composer-plugin-runtime.php @@ -0,0 +1,52 @@ + + */ + public static function getSubscribedEvents(): array; +} diff --git a/phpstan/stubs/legacy-plugin-aliases.php b/phpstan/stubs/legacy-plugin-aliases.php new file mode 100644 index 000000000..268086278 --- /dev/null +++ b/phpstan/stubs/legacy-plugin-aliases.php @@ -0,0 +1,59 @@ + + + + + plugin/think-library/tests/CodeTest.php + plugin/think-library/tests/JwtTest.php + plugin/think-library/tests/CommonFunctionsTest.php + plugin/think-library/tests/ArchitectureBoundaryTest.php + plugin/think-library/tests/ComposerInstallBoundaryTest.php + plugin/think-library/tests/RouteTemplateBoundaryTest.php + plugin/think-library/tests/MultAccessDispatchTest.php + plugin/think-library/tests/ComposerDependencyBoundaryTest.php + plugin/think-library/tests/MigrationOwnershipTest.php + plugin/think-library/tests/FormBuilderTest.php + plugin/think-library/tests/PageBuilderTest.php + plugin/think-library/tests/RequestTokenServiceTest.php + plugin/think-plugs-install/tests/InstallCommandTest.php + plugin/think-plugs-system/tests/helper/PublishTest.php + plugin/think-plugs-system/tests/helper/IndexNameServiceTest.php + plugin/think-plugs-system/tests/helper/PluginMenuServiceTest.php + plugin/think-plugs-system/tests/RbacAccessTest.php + plugin/think-plugs-system/tests/ApiSystemControllerTest.php + plugin/think-plugs-system/tests/ApiQueueControllerTest.php + plugin/think-plugs-system/tests/AuthControllerTest.php + plugin/think-plugs-system/tests/BaseControllerTest.php + plugin/think-plugs-system/tests/ConfigPageRenderTest.php + plugin/think-plugs-system/tests/IndexControllerTest.php + plugin/think-plugs-system/tests/LoginControllerTest.php + plugin/think-plugs-system/tests/MenuControllerTest.php + plugin/think-plugs-system/tests/OplogControllerTest.php + plugin/think-plugs-system/tests/PlugsControllerTest.php + plugin/think-plugs-system/tests/QueueControllerTest.php + plugin/think-plugs-system/tests/UploadControllerTest.php + plugin/think-plugs-system/tests/UserControllerTest.php + plugin/think-plugs-system/tests/FileControllerTest.php + plugin/think-plugs-system/tests/ConsoleCssUtilityTest.php + plugin/think-plugs-account/tests/AccountRuntimeTest.php + plugin/think-plugs-account/tests/AccountIntegrationTest.php + plugin/think-plugs-account/tests/AccountCenterControllerTest.php + plugin/think-plugs-account/tests/AccountAdminListControllerTest.php + plugin/think-plugs-payment/tests/PaymentTest.php + plugin/think-plugs-payment/tests/PaymentRecordControllerTest.php + plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php + plugin/think-plugs-payment/tests/BalanceIntegrationTest.php + plugin/think-plugs-payment/tests/IntegralIntegrationTest.php + plugin/think-plugs-payment/tests/PaymentRecordIntegrationTest.php + plugin/think-plugs-payment/tests/PaymentLedgerIntegrationTest.php + plugin/think-plugs-system/tests/CommonFunctionsTest.php + plugin/think-plugs-worker/tests/CommonFunctionsTest.php + plugin/think-plugs-worker/tests/ProcessServiceTest.php + plugin/think-plugs-worker/tests/WorkerConfigTest.php + plugin/think-plugs-worker/tests/QueueServiceTest.php + plugin/think-plugs-wuma/tests/CodeTest.php + plugin/think-plugs-wemall/tests/PaymentEventIntegrationTest.php + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 000000000..025086eec --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,69 @@ +setAccessible(true); + $reflection->setValue([]); + } +} diff --git a/tests/path-examples.php b/tests/path-examples.php new file mode 100644 index 000000000..2ce082820 --- /dev/null +++ b/tests/path-examples.php @@ -0,0 +1,140 @@ + 'runtime-publish/demo.txt', + ]); + mkdir($root . '/plugin/publish-demo/stc/runtime', 0777, true); + file_put_contents($root . '/plugin/publish-demo/stc/runtime/demo.txt', "publish\n"); + createPluginPackage( + $root, + 'system', + 'vendor/system-plugin', + 'plugin\system\Service', + [ + '20241010000001_install_system20241010.php', + '20241010000002_install_storage20241010.php', + ] + ); + createPluginPackage( + $root, + 'worker', + 'vendor/worker-plugin', + 'plugin\worker\Service', + '20241010000008_install_worker20241010.php' + ); + + $app = new App($root); + RuntimeService::init($app); + $app->config->set([ + 'default' => 'file', + 'stores' => [ + 'file' => ['type' => 'File', 'path' => $root . '/runtime/cache'], + ], + ], 'cache'); + + $command = new PublishCommand(); + $command->setApp($app); + + $code = $command->run(new Input([]), new Output('buffer')); + assertSameValue(0, $code, 'publish command should exit with code 0'); + + $services = require $root . '/vendor/services.php'; + $versions = require $root . '/vendor/versions.php'; + $manifest = json_decode((string)file_get_contents($root . '/database/migrations/.published.json'), true, 512, JSON_THROW_ON_ERROR); + $resourceManifest = json_decode((string)file_get_contents($root . '/vendor/.published.json'), true, 512, JSON_THROW_ON_ERROR); + + foreach (['plugin\demo\Service', 'plugin\system\Service', 'plugin\worker\Service'] as $service) { + assertTrue(in_array($service, $services, true), "missing published service {$service}"); + } + + assertSameValue('System', $versions['vendor/system-plugin']['name'] ?? null, 'system plugin name should be published'); + assertSameValue('plugin', $versions['vendor/system-plugin']['type'] ?? null, 'system plugin type should be published'); + assertTrue(!isset($versions['vendor/storage-plugin']), 'storage plugin should no longer be published separately'); + + foreach ([ + '20241010000001_install_system20241010.php', + '20241010000002_install_storage20241010.php', + '20241010000008_install_worker20241010.php', + ] as $migration) { + assertTrue(is_file($root . '/database/migrations/' . $migration), "missing migration {$migration}"); + } + + assertTrue( + is_file($root . '/database/migrations/20241011000001_install_wechat20241011.php'), + 'unmanaged unique migration should be preserved' + ); + + assertSameValue( + 'plugin/system/stc/database/20241010000001_install_system20241010.php', + $manifest['20241010000001_install_system20241010.php']['source'] ?? null, + 'system migration manifest should point to plugin source' + ); + assertTrue(is_file($root . '/runtime-publish/demo.txt'), 'missing published runtime resource'); + assertSameValue('vendor/publish-demo', $resourceManifest['runtime-publish/demo.txt']['package'] ?? null, 'runtime resource manifest package mismatch'); + + removeTree($root . '/plugin/publish-demo'); + + $command = new PublishCommand(); + $command->setApp($app); + $code = $command->run(new Input([]), new Output('buffer')); + assertSameValue(0, $code, 'second publish command should exit with code 0'); + + $resourceManifest = json_decode((string)file_get_contents($root . '/vendor/.published.json'), true, 512, JSON_THROW_ON_ERROR); + assertTrue(!is_file($root . '/runtime-publish/demo.txt'), 'stale published runtime resource should be removed'); + assertTrue(!isset($resourceManifest['runtime-publish/demo.txt']), 'stale runtime resource manifest should be removed'); + } finally { + removeTree($root); + } +} + +function runThinkListSmoke(string $projectRoot): void +{ + $command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($projectRoot . '/think') . ' list'; + exec($command . ' 2>&1', $output, $status); + + assertSameValue(0, $status, "think list failed:\n" . implode("\n", $output)); +} + +function runInstallSmoke(string $projectRoot): void +{ + $root = sys_get_temp_dir() . '/thinkadmin-install-' . bin2hex(random_bytes(6)); + + try { + mkdir($root, 0777, true); + copyTree($projectRoot . '/app', $root . '/app'); + copyTree($projectRoot . '/config', $root . '/config'); + copyTree($projectRoot . '/plugin', $root . '/plugin'); + copyTree($projectRoot . '/vendor', $root . '/vendor'); + copyFile($projectRoot . '/think', $root . '/think'); + copyFile($projectRoot . '/composer.json', $root . '/composer.json'); + + foreach (['database', 'public', 'runtime'] as $path) { + mkdir($root . '/' . $path, 0777, true); + } + + $command = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/think') . ' xadmin:publish --migrate'; + exec($command . ' 2>&1', $output, $status); + + assertSameValue(0, $status, "install smoke failed:\n" . implode("\n", $output)); + + foreach ([ + 'database/migrations/20241010000001_install_system20241010.php', + 'database/migrations/20241010000008_install_worker20241010.php', + 'public/static/system.js', + 'config/database.php', + ] as $path) { + assertTrue(is_file($root . '/' . $path), "missing installed artifact {$path}"); + } + + $db = new PDO('sqlite:' . $root . '/database/sqlite.db'); + foreach ([ + 'system_auth', + 'system_auth_node', + 'system_menu', + 'system_user', + 'system_data', + 'system_base', + 'system_oplog', + 'system_file', + 'system_queue', + ] as $table) { + $count = $db->query("select count(*) from sqlite_master where type='table' and name='{$table}'")->fetchColumn(); + assertTrue(!empty($count), "missing installed table {$table}"); + } + + $dataRows = intval($db->query('select count(*) from system_data')->fetchColumn()); + assertTrue($dataRows >= 4, 'system_data seed rows should be initialized'); + $db = null; + } finally { + removeTree($root); + } +} + +function assertHelperOwner(string $name, string $expectedFile): void +{ + assertTrue(function_exists($name), "{$name} should be defined"); + + $reflection = new ReflectionFunction($name); + + assertSameValue( + realpath($expectedFile), + realpath((string)$reflection->getFileName()), + "{$name} should be loaded from {$expectedFile}" + ); +} + +/** + * @param null|string|list $migration + */ +function createPluginPackage(string $root, string $code, string $name, string $service, null|string|array $migration = null, array $publish = []): void +{ + $path = "{$root}/plugin/{$code}"; + mkdir($path, 0777, true); + $extra = [ + 'think' => ['services' => [$service]], + 'xadmin' => ['app' => ['code' => $code, 'name' => ucfirst($code)]], + ]; + if ($publish !== []) { + $extra['xadmin']['publish'] = ['copy' => $publish]; + } + file_put_contents($path . '/composer.json', json_encode([ + 'name' => $name, + 'type' => 'think-admin-plugin', + 'version' => '1.0.0', + 'description' => ucfirst($code), + 'extra' => $extra, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + if ($migration) { + mkdir($path . '/stc/database', 0777, true); + foreach ((array)$migration as $filename) { + file_put_contents($path . '/stc/database/' . $filename, "getPathname(), 0777); + if ($item->isDir()) { + @rmdir($item->getPathname()); + } else { + @unlink($item->getPathname()); + } + } + + @rmdir($path); +} + +function copyTree(string $source, string $target): void +{ + if (!is_dir($source)) { + return; + } + + mkdir($target, 0777, true); + $items = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($items as $item) { + $relative = substr($item->getPathname(), strlen($source) + 1); + $pathname = $target . '/' . $relative; + if (is_link($item->getPathname())) { + $real = realpath($item->getPathname()); + if ($real === false) { + continue; + } + if (is_dir($real)) { + copyTree($real, $pathname); + } else { + copyFile($real, $pathname); + } + continue; + } + if ($item->isDir()) { + is_dir($pathname) || mkdir($pathname, 0777, true); + } else { + copyFile($item->getPathname(), $pathname); + } + } +} + +function copyFile(string $source, string $target): void +{ + $real = realpath($source); + if ($real !== false && is_dir($real)) { + copyTree($real, $target); + return; + } + if ($real !== false && is_file($real)) { + $source = $real; + } + if (is_dir($source)) { + copyTree($source, $target); + return; + } + is_dir(dirname($target)) || mkdir(dirname($target), 0777, true); + copy($source, $target); +} + +function assertTrue(bool $condition, string $message): void +{ + if ($condition) { + return; + } + + throw new RuntimeException($message); +} + +function assertSameValue($expected, $actual, string $message): void +{ + if ($expected === $actual) { + return; + } + + throw new RuntimeException( + $message . ' | expected: ' . var_export($expected, true) . ' actual: ' . var_export($actual, true) + ); +} + +function writeLine(string $message): void +{ + fwrite(STDOUT, $message . PHP_EOL); +} diff --git a/tests/support/SqliteIntegrationTestCase.php b/tests/support/SqliteIntegrationTestCase.php new file mode 100644 index 000000000..61e9a468a --- /dev/null +++ b/tests/support/SqliteIntegrationTestCase.php @@ -0,0 +1,1452 @@ +resetProcessState(); + + $this->sandboxPath = sys_get_temp_dir() . '/thinkadmin-sqlite-' . md5(static::class . microtime(true) . uniqid('', true)); + $this->databaseFile = $this->sandboxPath . '/database.sqlite'; + $this->connectionName = 'sqlite_' . md5($this->sandboxPath); + + if (!is_dir($this->sandboxPath)) { + mkdir($this->sandboxPath, 0777, true); + } + + touch($this->databaseFile); + $this->bootApplication(); + $this->defineSchema(); + $this->afterSchemaCreated(); + } + + protected function tearDown(): void + { + $this->resetProcessState(); + $this->restoreAccountTypes(); + RequestContext::clear(); + function_exists('sysvar') && sysvar('', ''); + Container::getInstance()->instance(SystemContextInterface::class, new NullSystemContext()); + + if (isset($this->app)) { + $this->closeDatabaseConnections(); + } + + $this->removeDirectory($this->sandboxPath); + parent::tearDown(); + } + + abstract protected function defineSchema(): void; + + protected function afterSchemaCreated(): void {} + + protected function connection(): ConnectionInterface + { + return $this->app->db->connect($this->connectionName); + } + + protected function executeStatements(array $statements): void + { + $connection = $this->connection(); + foreach ($statements as $statement) { + $connection->execute($statement); + } + } + + protected function configureView(array $overrides = []): void + { + $config = [ + 'type' => 'Think', + 'auto_rule' => 1, + 'view_dir_name' => 'view', + 'view_path' => '', + 'view_suffix' => 'html', + 'view_depr' => DIRECTORY_SEPARATOR, + 'tpl_cache' => false, + 'tpl_begin' => '{', + 'tpl_end' => '}', + 'taglib_begin' => '{', + 'taglib_end' => '}', + 'strip_space' => true, + 'default_filter' => 'htmlentities=###,ENT_QUOTES', + ]; + + $configFile = TEST_PROJECT_ROOT . '/config/view.php'; + if (is_file($configFile)) { + $config = array_merge($config, include $configFile); + } + + $this->app->config->set(array_merge($config, $overrides), 'view'); + } + + protected function activateApplicationContext(?Request $request = null): void + { + Container::setInstance($this->app); + Library::$sapp = $this->app; + if ($request instanceof Request) { + $this->app->instance('request', $request); + Container::getInstance()->instance('request', $request); + } + } + + protected function createAccountTables(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_account_user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT DEFAULT '', + phone TEXT DEFAULT '', + email TEXT DEFAULT '', + unionid TEXT DEFAULT '', + username TEXT DEFAULT '', + nickname TEXT DEFAULT '', + password TEXT DEFAULT '', + headimg TEXT DEFAULT '', + region_prov TEXT DEFAULT '', + region_city TEXT DEFAULT '', + region_area TEXT DEFAULT '', + remark TEXT DEFAULT '', + extra TEXT DEFAULT '', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + <<<'SQL' +CREATE TABLE plugin_account_bind ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + type TEXT DEFAULT '', + phone TEXT DEFAULT '', + appid TEXT DEFAULT '', + openid TEXT DEFAULT '', + unionid TEXT DEFAULT '', + headimg TEXT DEFAULT '', + nickname TEXT DEFAULT '', + password TEXT DEFAULT '', + extra TEXT DEFAULT '', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + <<<'SQL' +CREATE TABLE plugin_account_auth ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + usid INTEGER DEFAULT 0, + time INTEGER DEFAULT 0, + type TEXT DEFAULT '', + token TEXT DEFAULT '', + tokenv TEXT DEFAULT '', + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createPaymentBalanceTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_payment_balance ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + code TEXT DEFAULT '', + source_type TEXT DEFAULT '', + source_id TEXT DEFAULT '', + name TEXT DEFAULT '', + remark TEXT DEFAULT '', + amount NUMERIC DEFAULT 0.00, + amount_prev NUMERIC DEFAULT 0.00, + amount_next NUMERIC DEFAULT 0.00, + cancel INTEGER DEFAULT 0, + unlock INTEGER DEFAULT 0, + create_by INTEGER DEFAULT 0, + cancel_time TEXT DEFAULT NULL, + unlock_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createPaymentIntegralTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_payment_integral ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + code TEXT DEFAULT '', + source_type TEXT DEFAULT '', + source_id TEXT DEFAULT '', + name TEXT DEFAULT '', + remark TEXT DEFAULT '', + amount NUMERIC DEFAULT 0.00, + amount_prev NUMERIC DEFAULT 0.00, + amount_next NUMERIC DEFAULT 0.00, + cancel INTEGER DEFAULT 0, + unlock INTEGER DEFAULT 0, + create_by INTEGER DEFAULT 0, + cancel_time TEXT DEFAULT NULL, + unlock_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createPaymentRecordTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_payment_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + usid INTEGER DEFAULT 0, + code TEXT DEFAULT '', + order_no TEXT DEFAULT '', + order_name TEXT DEFAULT '', + order_amount NUMERIC DEFAULT 0.00, + channel_type TEXT DEFAULT '', + channel_code TEXT DEFAULT '', + payment_time TEXT DEFAULT NULL, + payment_trade TEXT DEFAULT '', + payment_status INTEGER DEFAULT 0, + payment_amount NUMERIC DEFAULT 0.00, + payment_coupon NUMERIC DEFAULT 0.00, + payment_images TEXT DEFAULT '', + payment_remark TEXT DEFAULT '', + payment_notify TEXT DEFAULT '', + audit_user INTEGER DEFAULT 0, + audit_time TEXT DEFAULT NULL, + audit_status INTEGER DEFAULT 1, + audit_remark TEXT DEFAULT '', + refund_status INTEGER DEFAULT 0, + refund_amount NUMERIC DEFAULT 0.00, + refund_payment NUMERIC DEFAULT 0.00, + refund_balance NUMERIC DEFAULT 0.00, + refund_integral NUMERIC DEFAULT 0.00, + used_payment NUMERIC DEFAULT 0.00, + used_balance NUMERIC DEFAULT 0.00, + used_integral NUMERIC DEFAULT 0.00, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemFileTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_file ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT DEFAULT '', + hash TEXT DEFAULT '', + tags TEXT DEFAULT '', + name TEXT DEFAULT '', + extension TEXT DEFAULT '', + xext TEXT DEFAULT '', + file_url TEXT DEFAULT '', + xurl TEXT DEFAULT '', + storage_key TEXT DEFAULT '', + xkey TEXT DEFAULT '', + mime TEXT DEFAULT '', + size INTEGER DEFAULT 0, + system_user_id INTEGER DEFAULT 0, + uuid INTEGER DEFAULT 0, + biz_user_id INTEGER DEFAULT 0, + unid INTEGER DEFAULT 0, + is_fast_upload INTEGER DEFAULT 0, + isfast INTEGER DEFAULT 0, + is_safe INTEGER DEFAULT 0, + issafe INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemBaseTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_base ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT DEFAULT '', + code TEXT DEFAULT '', + name TEXT DEFAULT '', + content TEXT DEFAULT '', + text_value TEXT DEFAULT '', + meta_json TEXT DEFAULT '', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + delete_time TEXT DEFAULT NULL, + deleted_by INTEGER DEFAULT 0, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemUserTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + base_code TEXT DEFAULT '', + username TEXT DEFAULT '', + password TEXT DEFAULT '', + nickname TEXT DEFAULT '', + headimg TEXT DEFAULT '', + auth_ids TEXT DEFAULT '', + contact_qq TEXT DEFAULT '', + contact_mail TEXT DEFAULT '', + contact_phone TEXT DEFAULT '', + login_ip TEXT DEFAULT '', + login_at TEXT DEFAULT '', + login_num INTEGER DEFAULT 0, + remark TEXT DEFAULT '', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + delete_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemAuthTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_auth ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT DEFAULT '', + code TEXT DEFAULT '', + remark TEXT DEFAULT '', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + create_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemAuthNodeTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_auth_node ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + auth INTEGER DEFAULT 0, + node TEXT DEFAULT '' +) +SQL, + ]); + } + + protected function createSystemMenuTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_menu ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pid INTEGER DEFAULT 0, + title TEXT DEFAULT '', + icon TEXT DEFAULT '', + node TEXT DEFAULT '', + url TEXT DEFAULT '', + params TEXT DEFAULT '', + target TEXT DEFAULT '_self', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + create_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemConfigTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT DEFAULT '', + name TEXT DEFAULT '', + value TEXT DEFAULT '' +) +SQL, + ]); + } + + protected function createSystemDataTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT DEFAULT '', + value TEXT DEFAULT '', + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemOplogTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_oplog ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + node TEXT DEFAULT '', + request_ip TEXT DEFAULT '', + geoip TEXT DEFAULT '', + action TEXT DEFAULT '', + content TEXT DEFAULT '', + username TEXT DEFAULT '', + create_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createSystemQueueTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE system_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT DEFAULT '', + exec_hash TEXT DEFAULT '', + title TEXT DEFAULT '', + command TEXT DEFAULT '', + exec_pid INTEGER DEFAULT 0, + exec_data TEXT DEFAULT '', + exec_time INTEGER DEFAULT 0, + exec_desc TEXT DEFAULT '', + enter_time NUMERIC DEFAULT 0.0000, + outer_time NUMERIC DEFAULT 0.0000, + loops_time INTEGER DEFAULT 0, + attempts INTEGER DEFAULT 0, + message TEXT DEFAULT '', + status INTEGER DEFAULT 1, + create_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createPaymentAddressTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_payment_address ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + type INTEGER DEFAULT 0, + user_name TEXT DEFAULT '', + user_phone TEXT DEFAULT '', + idcode TEXT DEFAULT '', + idimg1 TEXT DEFAULT '', + idimg2 TEXT DEFAULT '', + region_prov TEXT DEFAULT '', + region_city TEXT DEFAULT '', + region_area TEXT DEFAULT '', + region_addr TEXT DEFAULT '', + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createPaymentRefundTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_payment_refund ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + usid INTEGER DEFAULT 0, + code TEXT DEFAULT '', + record_code TEXT DEFAULT '', + refund_time TEXT DEFAULT NULL, + refund_trade TEXT DEFAULT '', + refund_status INTEGER DEFAULT 0, + refund_amount NUMERIC DEFAULT 0.00, + refund_account TEXT DEFAULT '', + refund_scode TEXT DEFAULT '', + refund_remark TEXT DEFAULT '', + refund_notify TEXT DEFAULT '', + used_payment NUMERIC DEFAULT 0.00, + used_balance NUMERIC DEFAULT 0.00, + used_integral NUMERIC DEFAULT 0.00, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallOrderTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_order ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + puid1 INTEGER DEFAULT 0, + puid2 INTEGER DEFAULT 0, + puid3 INTEGER DEFAULT 0, + order_no TEXT DEFAULT '', + status INTEGER DEFAULT 2, + delivery_type INTEGER DEFAULT 1, + payment_status INTEGER DEFAULT 0, + refund_status INTEGER DEFAULT 0, + cancel_status INTEGER DEFAULT 0, + deleted_status INTEGER DEFAULT 0, + amount_goods NUMERIC DEFAULT 0.00, + amount_discount NUMERIC DEFAULT 0.00, + amount_reduct NUMERIC DEFAULT 0.00, + amount_express NUMERIC DEFAULT 0.00, + amount_real NUMERIC DEFAULT 0.00, + amount_total NUMERIC DEFAULT 0.00, + payment_amount NUMERIC DEFAULT 0.00, + amount_payment NUMERIC DEFAULT 0.00, + amount_balance NUMERIC DEFAULT 0.00, + amount_integral NUMERIC DEFAULT 0.00, + rebate_amount NUMERIC DEFAULT 0.00, + reward_balance NUMERIC DEFAULT 0.00, + reward_integral NUMERIC DEFAULT 0.00, + level_agent INTEGER DEFAULT 0, + level_member INTEGER DEFAULT 0, + payment_time TEXT DEFAULT NULL, + deleted_remark TEXT DEFAULT '', + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallOrderItemTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_order_item ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + ssid INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + level_code INTEGER DEFAULT 0, + level_agent INTEGER DEFAULT 0, + level_upgrade INTEGER DEFAULT 0, + discount_id INTEGER DEFAULT 0, + rebate_type INTEGER DEFAULT 0, + stock_sales INTEGER DEFAULT 0, + delivery_count INTEGER DEFAULT 0, + order_no TEXT DEFAULT '', + gcode TEXT DEFAULT '', + ghash TEXT DEFAULT '', + gname TEXT DEFAULT '', + gcover TEXT DEFAULT '', + gunit TEXT DEFAULT '', + gspec TEXT DEFAULT '', + gsku TEXT DEFAULT '', + level_name TEXT DEFAULT '', + delivery_code TEXT DEFAULT '', + price_market NUMERIC DEFAULT 0.00, + price_selling NUMERIC DEFAULT 0.00, + amount_cost NUMERIC DEFAULT 0.00, + total_price_market NUMERIC DEFAULT 0.00, + total_price_selling NUMERIC DEFAULT 0.00, + total_price_cost NUMERIC DEFAULT 0.00, + discount_rate NUMERIC DEFAULT 0.00, + discount_amount NUMERIC DEFAULT 0.00, + total_allow_balance NUMERIC DEFAULT 0.00, + total_allow_integral NUMERIC DEFAULT 0.00, + rebate_amount NUMERIC DEFAULT 0.00, + total_reward_balance NUMERIC DEFAULT 0.00, + total_reward_integral NUMERIC DEFAULT 0.00, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallOrderSenderTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_order_sender ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + ssid INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + delivery_count INTEGER DEFAULT 0, + address_id TEXT DEFAULT '', + order_no TEXT DEFAULT '', + delivery_code TEXT DEFAULT '', + delivery_remark TEXT DEFAULT '', + company_code TEXT DEFAULT '', + company_name TEXT DEFAULT '', + express_code TEXT DEFAULT '', + express_remark TEXT DEFAULT '', + express_time TEXT DEFAULT NULL, + user_name TEXT DEFAULT '', + user_phone TEXT DEFAULT '', + user_idcode TEXT DEFAULT '', + user_idimg1 TEXT DEFAULT '', + user_idimg2 TEXT DEFAULT '', + region_prov TEXT DEFAULT '', + region_city TEXT DEFAULT '', + region_area TEXT DEFAULT '', + region_addr TEXT DEFAULT '', + extra TEXT DEFAULT '', + delivery_amount NUMERIC DEFAULT 0.00, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallUserRebateTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_user_rebate ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + order_unid INTEGER DEFAULT 0, + layer INTEGER DEFAULT 1, + status INTEGER DEFAULT 0, + code TEXT DEFAULT '', + hash TEXT DEFAULT '', + name TEXT DEFAULT '', + type TEXT DEFAULT '', + date TEXT DEFAULT '', + order_no TEXT DEFAULT '', + remark TEXT DEFAULT '', + amount NUMERIC DEFAULT 0.00, + order_amount NUMERIC DEFAULT 0.00, + confirm_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallUserTransferTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_user_transfer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + status INTEGER DEFAULT 0, + code TEXT DEFAULT '', + type TEXT DEFAULT '', + amount NUMERIC DEFAULT 0.00, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallConfigLevelTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_config_level ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + number INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + upgrade_team INTEGER DEFAULT 0, + upgrade_type INTEGER DEFAULT 0, + utime INTEGER DEFAULT 0, + name TEXT DEFAULT '', + remark TEXT DEFAULT '', + cardbg TEXT DEFAULT '', + cover TEXT DEFAULT '', + extra TEXT DEFAULT '', + delete_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallConfigAgentTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_config_agent ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + number INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + upgrade_type INTEGER DEFAULT 0, + utime INTEGER DEFAULT 0, + name TEXT DEFAULT '', + remark TEXT DEFAULT '', + cardbg TEXT DEFAULT '', + cover TEXT DEFAULT '', + extra TEXT DEFAULT '', + delete_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallUserCreateTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_user_create ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + agent_entry INTEGER DEFAULT 0, + phone TEXT DEFAULT '', + name TEXT DEFAULT '', + password TEXT DEFAULT '', + headimg TEXT DEFAULT '', + agent_phone TEXT DEFAULT '', + rebate_total NUMERIC DEFAULT 0.00, + rebate_usable NUMERIC DEFAULT 0.00, + rebate_total_code TEXT DEFAULT '', + rebate_total_desc TEXT DEFAULT '', + rebate_usable_code TEXT DEFAULT '', + rebate_usable_desc TEXT DEFAULT '', + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function createWemallUserRelationTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_wemall_user_relation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + layer INTEGER DEFAULT 0, + puid1 INTEGER DEFAULT 0, + puid2 INTEGER DEFAULT 0, + puid3 INTEGER DEFAULT 0, + puids INTEGER DEFAULT 0, + level_code INTEGER DEFAULT 0, + agent_level_code INTEGER DEFAULT 0, + entry_agent INTEGER DEFAULT 0, + entry_member INTEGER DEFAULT 0, + agent_state INTEGER DEFAULT 0, + agent_uuid INTEGER DEFAULT 0, + sort INTEGER DEFAULT 0, + path TEXT DEFAULT '', + level_name TEXT DEFAULT '', + agent_level_name TEXT DEFAULT '', + extra TEXT DEFAULT '', + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL, + delete_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function decimal($value): string + { + return number_format((float)$value, 2, '.', ''); + } + + protected function configureAccountAccess(array $overrides = []): void + { + $this->rememberAccountTypes(); + $this->restoreAccountTypes(); + $this->context->setData('plugin.account.access', array_merge([ + 'expire' => 3600, + 'headimg' => 'https://example.com/default-account.png', + 'userPrefix' => '测试账号', + ], $overrides)); + } + + protected function createAccountUser(array $overrides = []): PluginAccountUser + { + $user = PluginAccountUser::mk(); + $user->save(array_merge([ + 'code' => 'U' . random_int(100000, 999999), + 'phone' => $this->randomPhone(), + 'username' => 'user-' . random_int(100, 999), + 'nickname' => '测试用户', + 'extra' => [], + ], $overrides)); + + return $user->refresh(); + } + + protected function createAccountFixture(string $type = Account::WAP, array $data = []): AccountInterface + { + $field = Account::field($type) ?: 'phone'; + $value = strval($data[$field] ?? $this->makeAccountIdentity($field)); + + $account = Account::mk($type); + $account->set(array_merge([$field => $value], $data), false); + + return $account; + } + + protected function createBoundAccountFixture(string $type = Account::WAP, array $bindData = [], array $userData = []): AccountInterface + { + $field = Account::field($type) ?: 'phone'; + $account = $this->createAccountFixture($type, $bindData); + $current = $account->get(); + $identity = strval($current[$field] ?? $bindData[$field] ?? ''); + + $map = isset($userData['id']) ? ['id' => intval($userData['id'])] : [$field => strval($userData[$field] ?? $identity)]; + $payload = array_merge([ + 'username' => 'user-' . random_int(100, 999), + ], $userData); + + if (!isset($payload[$field]) && isset($map[$field])) { + $payload[$field] = $map[$field]; + } + + $account->bind($map, $payload); + return $account; + } + + protected function createPaidEmptyOrderFixture( + string $orderNo, + ?AccountInterface $account = null, + array $overrides = [] + ): PluginPaymentRecord { + $account ??= $this->createBoundAccountFixture(); + $response = Payment::mk(Payment::EMPTY)->create( + $account, + $orderNo, + strval($overrides['title'] ?? '退款测试订单'), + strval($overrides['order_amount'] ?? '10.00'), + strval($overrides['pay_amount'] ?? '10.00'), + strval($overrides['remark'] ?? '退款边界测试') + ); + + return PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + } + + protected function createWemallOrderFixture(AccountInterface $account, array $overrides = []): PluginWemallOrder + { + $order = PluginWemallOrder::mk(); + $order->save(array_merge([ + 'unid' => $account->getUnid(), + 'order_no' => 'ORDER-' . strtoupper(substr(md5(uniqid('', true)), 0, 10)), + 'status' => 2, + 'delivery_type' => 1, + 'payment_status' => 0, + 'refund_status' => 0, + 'cancel_status' => 0, + 'deleted_status' => 0, + 'puid1' => 0, + 'puid2' => 0, + 'puid3' => 0, + 'amount_goods' => '10.00', + 'amount_discount' => '10.00', + 'amount_reduct' => '0.00', + 'amount_express' => '0.00', + 'amount_real' => '10.00', + 'amount_total' => '10.00', + 'payment_amount' => '0.00', + 'amount_payment' => '0.00', + 'amount_balance' => '0.00', + 'amount_integral' => '0.00', + 'rebate_amount' => '0.00', + 'reward_balance' => '0.00', + 'reward_integral' => '0.00', + 'level_agent' => 0, + 'level_member' => 0, + ], $overrides)); + + return $order->refresh(); + } + + protected function createPaymentAddressFixture(int $unid, array $overrides = []): PluginPaymentAddress + { + $address = PluginPaymentAddress::mk(); + $address->save(array_merge([ + 'unid' => $unid, + 'type' => 1, + 'delete_time' => null, + 'user_name' => '测试收货人', + 'user_phone' => $this->randomPhone('1380013'), + 'idcode' => '110101199001010011', + 'idimg1' => '/upload/idcard-front.png', + 'idimg2' => '/upload/idcard-back.png', + 'region_prov' => '广东省', + 'region_city' => '深圳市', + 'region_area' => '南山区', + 'region_addr' => '科技园 1 号', + ], $overrides)); + + return $address->refresh(); + } + + protected function createSystemFileFixture(array $overrides = []): SystemFile + { + $file = SystemFile::mk(); + $file->save(SystemFile::syncPayload(array_merge([ + 'type' => 'local', + 'hash' => md5(uniqid('file', true)), + 'tags' => '', + 'name' => 'test-file.png', + 'extension' => 'png', + 'xext' => 'png', + 'file_url' => 'https://example.com/upload/test-file.png', + 'xurl' => 'https://example.com/upload/test-file.png', + 'storage_key' => 'upload/test-file.png', + 'xkey' => 'upload/test-file.png', + 'mime' => 'image/png', + 'size' => 1024, + 'system_user_id' => 0, + 'uuid' => 0, + 'biz_user_id' => 0, + 'unid' => 0, + 'is_fast_upload' => 0, + 'isfast' => 0, + 'is_safe' => 0, + 'issafe' => 0, + 'status' => 2, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ], $overrides))); + + return $file->refresh(); + } + + protected function createSystemBaseFixture(array $overrides = []): SystemBase + { + $base = SystemBase::mk(); + $base->save(array_merge([ + 'type' => 'identity', + 'code' => 'base-' . random_int(1000, 9999), + 'name' => '测试字典', + 'content' => '', + 'text_value' => '', + 'meta_json' => '', + 'sort' => 0, + 'status' => 1, + 'delete_time' => null, + 'deleted_by' => 0, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ], $overrides)); + + return $base->refresh(); + } + + protected function createSystemUserFixture(array $overrides = []): SystemUser + { + $data = array_merge([ + 'base_code' => '', + 'username' => 'admin-' . random_int(1000, 9999), + 'password' => $this->hashSystemPassword('123456'), + 'nickname' => '测试管理员', + 'headimg' => '', + 'auth_ids' => '', + 'contact_qq' => '', + 'contact_mail' => '', + 'contact_phone' => '', + 'login_ip' => '127.0.0.1', + 'login_at' => '', + 'login_num' => 0, + 'remark' => '', + 'sort' => 0, + 'status' => 1, + 'delete_time' => null, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ], $overrides); + $user = SystemUser::mk(); + if (isset($data['id']) && is_numeric($data['id'])) { + $user = SystemUser::mk()->withTrashed()->findOrEmpty(intval($data['id'])); + } + $user->save($data); + + return $user->refresh(); + } + + protected function hashSystemPassword(string $password): string + { + return UserService::hashPassword($password); + } + + protected function verifySystemPassword(string $password, ?string $hash): bool + { + return UserService::verifyPassword($password, $hash); + } + + protected function createSystemAuthFixture(array $overrides = []): SystemAuth + { + $auth = SystemAuth::mk(); + $auth->save(array_merge([ + 'title' => '测试权限', + 'code' => 'auth-' . random_int(1000, 9999), + 'remark' => '测试说明', + 'sort' => 0, + 'status' => 1, + 'create_time' => date('Y-m-d H:i:s'), + ], $overrides)); + + return $auth->refresh(); + } + + protected function createSystemAuthNodeFixture(array $overrides = []): SystemNode + { + $node = SystemNode::mk(); + $node->save(array_merge([ + 'auth' => 0, + 'node' => 'index/test/index', + ], $overrides)); + + return $node->refresh(); + } + + protected function createSystemMenuFixture(array $overrides = []): SystemMenu + { + $menu = SystemMenu::mk(); + $menu->save(array_merge([ + 'pid' => 0, + 'title' => '测试菜单', + 'icon' => 'layui-icon layui-icon-set', + 'node' => '', + 'url' => '#', + 'params' => '', + 'target' => '_self', + 'sort' => 0, + 'status' => 1, + 'create_time' => date('Y-m-d H:i:s'), + ], $overrides)); + + return $menu->refresh(); + } + + protected function createSystemConfigFixture(array $overrides = []): SystemConfig + { + $config = SystemConfig::mk(); + $config->save(array_merge([ + 'type' => 'base', + 'name' => 'site_name', + 'value' => 'ThinkAdmin', + ], $overrides)); + + return $config->refresh(); + } + + protected function createSystemDataFixture(array $overrides = []): SystemData + { + $data = SystemData::mk(); + $data->save(array_merge([ + 'name' => 'TestDataKey', + 'value' => json_encode([['ok' => true]], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ], $overrides)); + $this->flushSystemDataCache(); + + return $data->refresh(); + } + + protected function createSystemOplogFixture(array $overrides = []): SystemOplog + { + $oplog = SystemOplog::mk(); + $oplog->save(SystemOplog::syncPayload(array_merge([ + 'node' => 'system/test/index', + 'request_ip' => '', + 'geoip' => '127.0.0.1', + 'action' => '测试行为', + 'content' => '测试内容', + 'username' => 'tester', + 'create_time' => date('Y-m-d H:i:s'), + ], $overrides))); + + return $oplog->refresh(); + } + + protected function createSystemQueueFixture(array $overrides = []): SystemQueue + { + $data = array_merge([ + 'code' => 'Q' . strtoupper(substr(md5(uniqid('', true)), 0, 15)), + 'title' => '测试任务', + 'command' => 'xadmin:test queue', + 'exec_pid' => 0, + 'exec_data' => '{}', + 'exec_time' => time(), + 'exec_desc' => '', + 'enter_time' => '0.0000', + 'outer_time' => '0.0000', + 'loops_time' => 0, + 'attempts' => 0, + 'message' => '', + 'status' => 1, + 'create_time' => date('Y-m-d H:i:s'), + ], $overrides); + $queue = SystemQueue::mk()->where(['code' => $data['code']])->findOrEmpty(); + $queue->save($data); + $this->resetWorkerQueueServiceState(); + + return $queue->refresh(); + } + + protected function createWemallOrderItemFixture(AccountInterface $account, string $orderNo, array $overrides = []): PluginWemallOrderItem + { + $item = PluginWemallOrderItem::mk(); + $item->save(array_merge([ + 'unid' => $account->getUnid(), + 'status' => 1, + 'delete_time' => null, + 'order_no' => $orderNo, + 'delivery_count' => 1, + 'delivery_code' => 'NONE', + 'gcode' => 'GCODE-' . strtoupper(substr(md5(uniqid('', true)), 0, 8)), + 'ghash' => 'GHASH-' . strtoupper(substr(md5(uniqid('', true)), 0, 8)), + 'gname' => '测试商品', + 'gcover' => '/upload/goods.png', + 'gunit' => '件', + 'gspec' => '默认规格', + 'gsku' => 'SKU-001', + 'price_market' => '10.00', + 'price_selling' => '10.00', + 'amount_cost' => '5.00', + 'total_price_market' => '10.00', + 'total_price_selling' => '10.00', + 'total_price_cost' => '5.00', + 'discount_rate' => '100.00', + 'discount_amount' => '0.00', + 'total_allow_balance' => '0.00', + 'total_allow_integral' => '0.00', + 'rebate_amount' => '0.00', + 'total_reward_balance' => '0.00', + 'total_reward_integral' => '0.00', + ], $overrides)); + + return $item->refresh(); + } + + protected function createWemallRelationFixture(int $unid, array $overrides = []): PluginWemallUserRelation + { + $relation = PluginWemallUserRelation::mk(); + $relation->save(array_merge([ + 'unid' => $unid, + 'layer' => 0, + 'puid1' => 0, + 'puid2' => 0, + 'puid3' => 0, + 'puids' => 0, + 'level_code' => 0, + 'agent_level_code' => 0, + 'entry_agent' => 0, + 'entry_member' => 0, + 'path' => ',', + 'level_name' => '普通用户', + 'agent_level_name' => '会员用户', + 'extra' => [], + ], $overrides)); + + return $relation->refresh(); + } + + protected function randomPhone(string $prefix = '1360013'): string + { + return $prefix . random_int(1000, 9999); + } + + private function bootApplication(): void + { + function_exists('test_reset_model_makers') && test_reset_model_makers(); + $this->app = new App(TEST_PROJECT_ROOT); + RuntimeService::init($this->app); + $this->activateApplicationContext(); + + $this->app->config->set([ + 'default' => 'file', + 'stores' => [ + 'file' => [ + 'type' => 'File', + 'path' => $this->sandboxPath . '/cache', + ], + ], + ], 'cache'); + $this->app->config->set([ + 'default' => 'file', + 'channels' => [ + 'file' => [ + 'type' => 'File', + 'path' => $this->sandboxPath . '/log', + 'single' => true, + 'apart_level' => [], + 'max_files' => 0, + 'json' => false, + 'format' => '[%s][%s] %s', + 'realtime_write' => true, + ], + ], + ], 'log'); + $this->app->config->set(['jwtkey' => 'integration-test-jwt'], 'app'); + $this->app->config->set([ + 'default' => $this->connectionName, + 'auto_timestamp' => true, + 'datetime_format' => 'Y-m-d H:i:s', + 'connections' => [ + $this->connectionName => [ + 'type' => 'sqlite', + 'database' => $this->databaseFile, + 'charset' => 'utf8', + 'trigger_sql' => false, + 'deploy' => 0, + 'suffix' => '', + 'prefix' => '', + 'hostname' => '', + 'hostport' => '', + 'username' => '', + 'password' => '', + ], + ], + ], 'database'); + $this->app->db->setConfig($this->app->config); + $this->app->db->connect($this->connectionName)->execute('PRAGMA busy_timeout = 5000'); + ThinkModel::maker(function (ThinkModel $model): void { + $db = $this->app->db; + $model->setOption('db', $db); + if (is_null($model->getAutoWriteTimestamp())) { + $model->isAutoWriteTimestamp($db->getConfig('auto_timestamp', true)); + } + if (is_null($model->getDateFormat())) { + $model->setDateFormat($db->getConfig('datetime_format', 'Y-m-d H:i:s')); + } + }); + + $this->context = new TestSystemContext(); + if (interface_exists(StorageManagerInterface::class) && class_exists(StorageManager::class)) { + Container::getInstance()->bind(StorageManagerInterface::class, StorageManager::class); + } + Container::getInstance()->instance(SystemContextInterface::class, $this->context); + RequestContext::clear(); + function_exists('sysvar') && sysvar('', ''); + } + + private function resetProcessState(): void + { + RequestContext::clear(); + function_exists('sysvar') && sysvar('', ''); + AppService::clear(); + AuthService::removeCheckCallable(null); + $this->resetWorkerQueueServiceState(); + } + + private function flushSystemDataCache(): void + { + function_exists('sysvar') && sysvar('think.admin.data', []); + if (isset($this->app)) { + try { + $this->app->cache->delete('SystemData'); + } catch (\Throwable) { + } + } + } + + private function resetWorkerQueueServiceState(): void + { + try { + $property = new \ReflectionProperty(WorkerQueueService::class, 'tableFields'); + $property->setAccessible(true); + $property->setValue(null, null); + } catch (\Throwable) { + } + } + + private function rememberAccountTypes(): void + { + if ($this->accountTypesSnapshot !== null) { + return; + } + + $this->accountTypesSnapshot = (new \ReflectionClass(Account::class))->getDefaultProperties()['types']; + } + + private function restoreAccountTypes(): void + { + if ($this->accountTypesSnapshot === null) { + return; + } + + $reflection = new \ReflectionClass(Account::class); + + $types = $reflection->getProperty('types'); + $types->setValue(null, $this->accountTypesSnapshot); + + $denys = $reflection->getProperty('denys'); + $denys->setValue(null, null); + } + + private function makeAccountIdentity(string $field): string + { + if ($field === 'phone') { + return $this->randomPhone(); + } + + return $field . '-' . uniqid(); + } + + private function removeDirectory(string $path): void + { + if ($path === '' || !is_dir($path)) { + return; + } + + foreach (scandir($path) ?: [] as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $target = $path . DIRECTORY_SEPARATOR . $item; + if (is_dir($target)) { + $this->removeDirectory($target); + } elseif (is_file($target)) { + @unlink($target); + } + } + + @rmdir($path); + } + + private function closeDatabaseConnections(): void + { + try { + $manager = $this->app->db; + $reflection = new \ReflectionObject($manager); + if (!$reflection->hasProperty('instance')) { + return; + } + + $property = $reflection->getProperty('instance'); + $property->setAccessible(true); + foreach ((array)$property->getValue($manager) as $connection) { + if ($connection instanceof ConnectionInterface) { + try { + $connection->close(); + } catch (\Throwable) { + } + } + } + $property->setValue($manager, []); + } catch (\Throwable) { + try { + $this->app->db->connect($this->connectionName)->close(); + } catch (\Throwable) { + } + } + } +} diff --git a/tests/support/TestSystemContext.php b/tests/support/TestSystemContext.php new file mode 100644 index 000000000..311735a2f --- /dev/null +++ b/tests/support/TestSystemContext.php @@ -0,0 +1,141 @@ +super) { + return true; + } + return $node !== null && in_array($node, $this->nodes, true); + } + + public function getUser(?string $field = null, $default = null) + { + if ($field === null) { + return $this->user; + } + + return $this->user[$field] ?? $default; + } + + public function getUserId(): int + { + return $this->userId; + } + + public function isSuper(): bool + { + return $this->super; + } + + public function isLogin(): bool + { + return $this->login; + } + + public function withUploadUnid(?string $uptoken = null): array + { + return [0, []]; + } + + public function clearAuth(): bool + { + return true; + } + + public function getData(string $name, $default = []) + { + return $this->data[$name] ?? $default; + } + + public function setData(string $name, $value): bool + { + $this->data[$name] = $value; + return true; + } + + public function setOplog(string $action, string $content): bool + { + return true; + } + + public function baseItems(string $type, array &$data = [], string $field = 'base_code', string $bind = 'base_info'): array + { + return []; + } + + public function setUser(array $user = [], bool $login = false, bool $super = false): self + { + $this->user = $user; + $this->userId = intval($user['id'] ?? 0); + $this->login = $login; + $this->super = $super; + return $this; + } + + public function setNodes(array $nodes = []): self + { + $this->nodes = array_values(array_unique(array_filter(array_map('strval', $nodes)))); + return $this; + } +}