banner_site_publication.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. <?php
  2. namespace app\api\lists {
  3. // Isolate list infrastructure; run the real list implementation and ThinkPHP ORM below.
  4. class BaseApiDataLists {
  5. protected string $lang = 'zh';
  6. protected array $searchWhere = [];
  7. public int $limitOffset = 0;
  8. public int $limitLength = 2;
  9. public function __construct(array $params = []) { $this->setParams($params); }
  10. public function setParams($params): self {
  11. $this->lang = $params['lang'] ?? 'zh';
  12. $this->searchWhere = isset($params['type']) ? [['type', '=', $params['type']]] : [];
  13. return $this;
  14. }
  15. }
  16. function time(): int { return $GLOBALS['banner_test_now']; }
  17. }
  18. namespace {
  19. error_reporting(E_ALL & ~E_DEPRECATED);
  20. function env($key, $default = '') {
  21. return ['app.host' => 'https://site.example.test', 'app.BOT_HOST' => 'https://bot.example.test'][$key] ?? $default;
  22. }
  23. $repo = getenv('BANNER_SITE_REPO') ?: dirname(__DIR__, 3) . '/melbet_sport-api';
  24. $stage = getenv('BANNER_SITE_STAGE') ?: $repo;
  25. $loader = require $repo . '/vendor/autoload.php';
  26. $loader->setPsr4('app\\', [$stage . '/app/', $repo . '/app/']);
  27. require $repo . '/app/common.php';
  28. $app = new \think\App($repo . '/');
  29. \think\Container::setInstance($app);
  30. $db = new \think\DbManager();
  31. $db->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [
  32. 'type' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'fields_strict' => false, 'debug' => false,
  33. ]], 'auto_timestamp' => false]);
  34. $app->instance(\think\DbManager::class, $db);
  35. $app->instance('log', new \Psr\Log\NullLogger());
  36. $selectCount = 0;
  37. $db->event('before_select', static function () use (&$selectCount) { ++$selectCount; });
  38. $GLOBALS['banner_test_now'] = 1792800000;
  39. $now = $GLOBALS['banner_test_now'];
  40. $db->execute('CREATE TABLE bot_banner (id INTEGER PRIMARY KEY,title TEXT,image TEXT,link TEXT,type TEXT,sort INTEGER,lang TEXT,status INTEGER DEFAULT 1)');
  41. $db->execute('CREATE TABLE bot_activity_rewards (id INTEGER PRIMARY KEY,status INTEGER,start_time INTEGER,end_time INTEGER)');
  42. foreach ([
  43. [101, 1, $now - 100, $now + 100],
  44. [102, 1, $now - 100, $now],
  45. [103, 1, $now + 1, $now + 100],
  46. [104, 0, $now - 100, $now + 100],
  47. [105, 1, $now, $now + 100],
  48. ] as [$id, $status, $start, $end]) {
  49. \think\facade\Db::table('bot_activity_rewards')->insert(['id' => $id, 'status' => $status, 'start_time' => $start, 'end_time' => $end]);
  50. }
  51. function bannerRow(int $id, $link, array $overrides = []): void {
  52. \think\facade\Db::table('bot_banner')->insert($overrides + [
  53. 'id' => $id, 'title' => 'Banner ' . $id, 'image' => 'https://site.example.test/banner.png',
  54. 'link' => $link, 'type' => 'pc', 'sort' => 100 - $id, 'lang' => 'zh', 'status' => 1,
  55. ]);
  56. }
  57. bannerRow(1, '102'); // Expired first-ranked item must not create a pagination gap.
  58. bannerRow(2, '', ['status' => 0]);
  59. bannerRow(3, '101');
  60. bannerRow(4, '');
  61. bannerRow(5, '0');
  62. bannerRow(6, 'https://external.example.test/path');
  63. bannerRow(7, '999');
  64. bannerRow(8, '103');
  65. bannerRow(9, '104');
  66. bannerRow(10, '105');
  67. bannerRow(11, '', ['lang' => 'en']);
  68. bannerRow(12, '', ['type' => 'app']);
  69. bannerRow(13, null);
  70. bannerRow(14, '000101');
  71. bannerRow(15, '000');
  72. bannerRow(16, '101', ['status' => 0]);
  73. $checks = 0;
  74. function check($actual, $expected, string $label): void {
  75. global $checks;
  76. ++$checks;
  77. if ($actual !== $expected) throw new \RuntimeException($label . ': got ' . json_encode($actual) . ', expected ' . json_encode($expected));
  78. }
  79. $before = \think\facade\Db::table('bot_banner')->order('id')->select()->toArray();
  80. $list = new \app\api\lists\BannerLists(['type' => 'pc']);
  81. $queryCount = $selectCount;
  82. check($list->count(), 9, 'Unbound banners use manual status; bound banners use only activity lifecycle');
  83. check($selectCount - $queryCount, 2, 'One banner scan and one activity lookup');
  84. $queryCount = $selectCount;
  85. check(array_column($list->lists(), 'id'), [3, 4], 'Lifecycle filtering precedes pagination');
  86. check($selectCount, $queryCount, 'Count and page share cached visibility');
  87. $list->limitOffset = 2;
  88. check(array_column($list->lists(), 'id'), [5, 6], 'Zero and external URL are unbound');
  89. $list->limitOffset = 4;
  90. check(array_column($list->lists(), 'id'), [10, 13], 'Start boundary and null link are visible');
  91. $list->limitOffset = 6;
  92. check(array_column($list->lists(), 'id'), [14, 15], 'Leading zeros match the correct positive ID or unbound zero');
  93. $list->limitOffset = 8;
  94. check(array_column($list->lists(), 'id'), [16], 'Archived manual-off cannot hide a banner bound to an active activity');
  95. $list->limitOffset = 0;
  96. $item = $list->lists()[0];
  97. check(array_keys($item), ['id', 'type', 'image', 'link', 'title'], 'Public fields remain compatible');
  98. check($item['image'], 'https://bot.example.test/banner.png', 'Existing image host replacement is preserved');
  99. check($item['link'], '101', 'Public jump target preserved');
  100. check(\think\facade\Db::table('bot_banner')->order('id')->select()->toArray(), $before, 'Reads do not mutate manual status or other original fields');
  101. $english = new \app\api\lists\BannerLists(['type' => 'pc', 'lang' => 'en']);
  102. check(array_column($english->lists(), 'id'), [11], 'Language filter retained');
  103. $appList = new \app\api\lists\BannerLists(['type' => 'app']);
  104. check(array_column($appList->lists(), 'id'), [12], 'Platform filter retained');
  105. $english->setParams(['type' => 'app']);
  106. check(array_column($english->lists(), 'id'), [12], 'setParams clears cached rows and restores query filters');
  107. \think\facade\Db::table('bot_activity_rewards')->where('id', 101)->update(['status' => 0]);
  108. check((new \app\api\lists\BannerLists(['type' => 'pc']))->count(), 6, 'Activity off hides every linked banner regardless of archived manual status');
  109. \think\facade\Db::table('bot_activity_rewards')->where('id', 101)->update(['status' => 1]);
  110. check((new \app\api\lists\BannerLists(['type' => 'pc']))->count(), 9, 'Activity on restores every linked banner including archived manual-off');
  111. // Reopen an activity: its linked banner returns, while unrelated unbound manual-off banners stay hidden.
  112. \think\facade\Db::table('bot_activity_rewards')->where('id', 102)->update(['end_time' => $now + 100]);
  113. $fresh = new \app\api\lists\BannerLists(['type' => 'pc']);
  114. check($fresh->count(), 10, 'Activity extension immediately restores eligible banner on the next request');
  115. check(array_column($fresh->lists(), 'id'), [1, 3], 'Extended activity returns to original order');
  116. check($list->count(), 9, 'One response keeps count and lists consistent across concurrent changes');
  117. $GLOBALS['banner_test_now'] = $now + 100;
  118. $ended = new \app\api\lists\BannerLists(['type' => 'pc']);
  119. $ended->limitLength = 100;
  120. check(array_column($ended->lists(), 'id'), [4, 5, 6, 13, 15], 'All bound banners stop exactly at end time while unbound remain');
  121. \think\facade\Db::table('bot_banner')->where('id', 4)->update(['status' => 0]);
  122. check((new \app\api\lists\BannerLists(['type' => 'pc']))->count(), 4, 'Manual off takes effect on the next request');
  123. \think\facade\Db::table('bot_banner')->where('id', 4)->update(['status' => 1]);
  124. check((new \app\api\lists\BannerLists(['type' => 'pc']))->count(), 5, 'Manual on restores unbound banner');
  125. echo 'PASS ' . $checks . " banner site assertions\n";
  126. }