sharing_mysql_support.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. <?php
  2. namespace Tests\Fixtures;
  3. /** Test-only bootstraps. Only the unique temporary, socket-only server is ever accepted. */
  4. final class SharingMysqlSupport
  5. {
  6. public static function guardedPdo(string $socket, ?string $database = null): \PDO
  7. {
  8. if (!preg_match('~^/private/tmp/bot28-sharing-mysql\.[A-Za-z0-9]{6}/mysql\.sock$~D', $socket)) {
  9. throw new \RuntimeException('Refusing any socket outside a unique sharing MySQL test directory');
  10. }
  11. if ($database !== null && !preg_match('/^share_concurrency_[a-f0-9]{12}$/D', $database)) {
  12. throw new \RuntimeException('Refusing a non-test database');
  13. }
  14. $pdo = new \PDO('mysql:unix_socket=' . $socket . ';charset=utf8mb4' . ($database ? ';dbname=' . $database : ''), 'root', '', [
  15. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_TIMEOUT => 3,
  16. ]);
  17. $server = $pdo->query('SELECT @@datadir AS data_dir, @@socket AS socket_path, @@skip_networking AS network_disabled')->fetch(\PDO::FETCH_ASSOC);
  18. if (realpath(rtrim($server['data_dir'], '/')) !== realpath(dirname($socket) . '/data')
  19. || $server['socket_path'] !== $socket || (int)$server['network_disabled'] !== 1) {
  20. throw new \RuntimeException('Refusing a server whose datadir, socket or skip-networking does not match the fixture');
  21. }
  22. return $pdo;
  23. }
  24. public static function laravel(string $socket, string $database): \Illuminate\Foundation\Application
  25. {
  26. self::guardedPdo($socket, $database);
  27. \Illuminate\Support\Facades\Facade::clearResolvedInstances();
  28. $app = new \Illuminate\Foundation\Application(dirname(__DIR__, 2));
  29. $app->instance('config', new \Illuminate\Config\Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai'], 'third_game' => ['currency' => 'CNY']]));
  30. $app->instance('events', new \Illuminate\Events\Dispatcher($app));
  31. \Illuminate\Support\Facades\Facade::setFacadeApplication($app);
  32. $db = new \Illuminate\Database\Capsule\Manager($app);
  33. $db->addConnection(['driver' => 'mysql', 'unix_socket' => $socket, 'database' => $database,
  34. 'username' => 'root', 'password' => '', 'prefix' => 'bot_', 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'strict' => true]);
  35. $db->setEventDispatcher($app['events']);
  36. $db->setAsGlobal(); $db->bootEloquent();
  37. $app->instance('db', $db->getDatabaseManager());
  38. $app->bind('db.schema', fn () => $db->schema());
  39. $translator = new \Illuminate\Translation\Translator(new \Illuminate\Translation\ArrayLoader(), 'zh');
  40. $app->instance('translator', $translator);
  41. $app->instance('validator', new \Illuminate\Validation\Factory($translator, $app));
  42. $request = \Illuminate\Http\Request::create('/isolated-sharing-mysql-fixture');
  43. $request->user = (object)['id' => 8, 'username' => 'fixture-admin'];
  44. $app->instance('request', $request);
  45. \Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2026-10-25 12:00:00', 'Asia/Shanghai'));
  46. return $app;
  47. }
  48. public static function schema(): void
  49. {
  50. $schema = \Illuminate\Support\Facades\DB::connection()->getSchemaBuilder();
  51. $schema->create('users', function ($t) {
  52. $t->id(); $t->string('user_id'); $t->string('member_id')->unique(); $t->string('account'); $t->string('first_name');
  53. $t->string('user_code'); $t->string('agent_user_code')->nullable(); $t->integer('from')->default(1);
  54. $t->dateTime('delete_time')->nullable(); $t->timestamps();
  55. });
  56. $schema->create('share_settings', function ($t) {
  57. $t->id(); $t->boolean('enabled')->default(0); $t->boolean('benefit_enabled')->default(0); $t->string('share_domain')->default('');
  58. $t->decimal('valid_member_min_deposit', 20, 2)->default(20); $t->decimal('non_seller_threshold', 20, 2)->default(200);
  59. $t->decimal('tier_one_rate', 8, 4)->default(4); $t->decimal('tier_two_rate', 8, 4)->default(6);
  60. $t->unsignedBigInteger('operator_id')->nullable(); $t->string('operator_name')->default(''); $t->timestamps();
  61. });
  62. $schema->create('wallets', function ($t) {
  63. $t->id(); $t->string('member_id')->unique(); $t->decimal('available_balance', 24, 10)->default(20); $t->timestamps();
  64. });
  65. $schema->create('balance_logs', function ($t) {
  66. $t->id(); $t->string('member_id'); $t->integer('type')->default(1); $t->string('change_type');
  67. foreach (['amount', 'before_balance', 'after_balance'] as $field) $t->decimal($field, 24, 10);
  68. $t->unsignedBigInteger('related_id')->nullable(); $t->text('remark')->nullable(); $t->timestamps();
  69. });
  70. $schema->create('operation_audits', function ($t) {
  71. $t->id(); $t->string('resource_type'); $t->unsignedBigInteger('resource_id')->nullable(); $t->string('action'); $t->json('changes');
  72. $t->unsignedBigInteger('operator_id')->nullable(); $t->string('operator_name'); $t->timestamp('created_at')->useCurrent();
  73. });
  74. (require dirname(__DIR__, 2) . '/database/migrations/2026_10_24_120000_create_member_sharing_system.php')->up();
  75. }
  76. public static function seed(): void
  77. {
  78. $db = '\\Illuminate\\Support\\Facades\\DB';
  79. $old = '2026-10-01 00:00:00';
  80. foreach ([1, 2] as $id) {
  81. $db::table('users')->insert(['id' => $id, 'user_id' => (string)(100 + $id), 'member_id' => 'M' . $id,
  82. 'account' => 'member' . $id, 'first_name' => 'fixture' . $id, 'user_code' => 'CODE' . $id,
  83. 'agent_user_code' => $id === 2 ? 'CODE1' : null, 'from' => 1, 'created_at' => $old, 'updated_at' => $old]);
  84. $db::table('share_members')->insert(['user_id' => $id, 'parent_user_id' => $id === 2 ? 1 : null,
  85. 'invitation_code' => 'CODE' . $id, 'binding_status' => $id === 2 ? 'matched' : 'root', 'level' => 1,
  86. 'bound_at' => $old, 'created_at' => $old, 'updated_at' => $old]);
  87. $db::table('share_tree')->insert(['ancestor_user_id' => $id, 'descendant_user_id' => $id, 'depth' => 0, 'bound_at' => $old]);
  88. }
  89. $db::table('share_tree')->insert(['ancestor_user_id' => 1, 'descendant_user_id' => 2, 'depth' => 1, 'bound_at' => $old]);
  90. $db::table('wallets')->insert(['member_id' => 'M1', 'available_balance' => '20', 'created_at' => $old, 'updated_at' => $old]);
  91. $db::table('share_accounts')->insert(['user_id' => 1, 'earned_amount' => '10.019', 'paid_amount' => 0, 'reserved_amount' => 0,
  92. 'created_at' => $old, 'updated_at' => $old]);
  93. $db::table('share_system_state')->insert(['key' => 'initialized_at', 'value' => $old, 'created_at' => $old, 'updated_at' => $old]);
  94. $db::table('share_settings')->insert(['enabled' => 0, 'benefit_enabled' => 1, 'share_domain' => 'https://fixture.example',
  95. 'activation_date' => '2026-10-24', 'valid_member_min_deposit' => 0, 'valid_member_min_effective_bet' => 0]);
  96. $rules = array_fill_keys(array_keys(\App\Services\Sharing\ShareDefaults::categories()), [['min_valid_members' => 0, 'min_performance' => '0', 'rate' => '10']]);
  97. $db::table('share_policies')->insert(['effective_date' => '2026-10-24', 'enabled' => 0, 'benefit_enabled' => 1,
  98. 'valid_member_min_deposit' => 0, 'valid_member_min_effective_bet' => 0, 'rules' => json_encode($rules)]);
  99. $db::table('share_bet_facts')->insert(['source' => 'fixture', 'source_id' => 'fact-1', 'user_id' => 2, 'member_id' => 'M2',
  100. 'category' => 'third:2', 'bet_at' => '2026-10-24 12:00:00', 'bet_date' => '2026-10-24', 'settled_at' => '2026-10-25 10:00:00',
  101. 'bet_amount' => 100, 'net_profit' => -100, 'effective_amount' => 100, 'settled' => 1, 'voided' => 0, 'currency' => 'CNY',
  102. 'source_version' => str_repeat('a', 64), 'created_at' => $old, 'updated_at' => $old]);
  103. }
  104. public static function hold(array $params): void
  105. {
  106. file_put_contents($params['locked'], 'locked');
  107. $deadline = microtime(true) + 20;
  108. while (!is_file($params['release'])) {
  109. if (microtime(true) > $deadline) throw new \RuntimeException('Timed out waiting for the test to release its row lock');
  110. usleep(10000);
  111. }
  112. }
  113. }