doge 16 годин тому
батько
коміт
659178782c

+ 125 - 0
docs/前端接口/活动/会员活动列表类型与游戏类型返回-前端接入说明.md

@@ -0,0 +1,125 @@
+# 会员活动列表的活动类型与游戏类型-前端接入说明
+
+适用接口:
+
+- `GET /api/index/activity`:会员活动列表。
+- `GET /api/index/activityInfo`:活动详情。
+
+URL 相对会员 API 域名,继续使用现有请求封装和部署环境的加解密流程。下例均为解密后的业务 JSON,会员端成功码为 `code=1`。
+
+## 1. 返回字段
+
+两个接口返回相同的类型字段:列表位于 `data.list[]`,详情位于 `data`。
+
+| 字段 | 类型 | 用途 |
+|---|---|---|
+| `type` | 原活动类型 ID | 原活动业务类型,不改变编号 |
+| `type_name` | string 或 `null` | 活动类型名称;`2` 显示“普通活动” |
+| `game_category` | string 或 `null` | 活动在后台关联的游戏分类 |
+| `game_category_name` | string 或 `null` | 游戏分类展示名称 |
+
+前端直接使用两个名称字段显示文字,使用原始 `type/game_category` 判断分类。不要把活动类型和游戏类型混成一个值。
+
+### 活动类型
+
+| `type` | `type_name` |
+|---|---|
+| `2` | 普通活动 |
+| `3` | 即充即送 |
+| `4` | 老用户召集令 |
+| `5` | USDT使用教学 |
+| `6` | 余额宝 |
+
+历史 `type=1` 及未知活动类型保留原 ID,`type_name=null`,不自动改成普通活动。原“足球首页”只改显示名称,编号和既有业务规则保留。
+
+### 游戏类型
+
+| `game_category` | `game_category_name` |
+|---|---|
+| `third:1` | 真人 |
+| `third:2` | 电子 |
+| `third:3` | 彩票 |
+| `third:4` | 体育 |
+| `third:5` | 电竞 |
+| `third:6` | 捕鱼 |
+| `third:7` | 棋牌 |
+| `self:sport` | F1体育 |
+| `self:lottery` | F1彩票 |
+| `other` | 其他 |
+
+后台没有设置游戏分类时,`game_category` 和 `game_category_name` 均为 `null`,前端按未分类处理,不自动归入“其他”。遇到未知历史分类时保留原值,名称为 `null`。
+
+游戏类型是活动的展示分类,不改变活动原有奖励、流水或资格规则。
+
+## 2. 活动列表
+
+请求示例:
+
+`GET /api/index/activity?page=1&limit=20&page_type=1&platform=H5`
+
+沿用原 `Lang` 请求头和会员登录态。当前客户端平台统一使用 `PC/H5/APP`;可选 `type` 及其原有筛选含义保持不变。分页继续使用 `page`、`limit`、`page_type`,没有新增必填参数。
+
+下例保留主要展示字段,其他原有字段按实际响应读取:
+
+```json
+{
+  "code": 1,
+  "show": 0,
+  "msg": "",
+  "data": {
+    "count": 1,
+    "list": [
+      {
+        "id": 123,
+        "title": "游戏主题活动",
+        "sub_title": "活动详情",
+        "type": 2,
+        "type_name": "普通活动",
+        "game_category": "other",
+        "game_category_name": "其他",
+        "is_eligible": true,
+        "ineligible_reason": ""
+      }
+    ],
+    "extend": []
+  }
+}
+```
+
+`count` 仍为按现有可见性规则筛选后的总数。类型名称不会改变排序、分页和会员资格结果。
+
+## 3. 活动详情
+
+`GET /api/index/activityInfo?id=123&platform=H5`
+
+返回结构保持详情对象,同样补齐两个名称字段。以下仅展示部分字段:
+
+```json
+{
+  "code": 1,
+  "show": 0,
+  "msg": "",
+  "data": {
+    "id": 123,
+    "title": "游戏主题活动",
+    "type": 2,
+    "type_name": "普通活动",
+    "game_category": "other",
+    "game_category_name": "其他",
+    "is_eligible": true,
+    "ineligible_reason": ""
+  }
+}
+```
+
+详情继续执行平台、会员资格及通道组校验;不能通过直接传活动 ID 查看原本不可见的活动。
+
+## 4. 前端联调
+
+1. 后台将活动关联为“电子”,列表与详情都返回 `third:2/电子`。
+2. 后台改为“其他”,刷新后两个接口都返回 `other/其他`。
+3. 后台清空游戏分类,两个接口均返回 `null/null`,不自动显示其他类别。
+4. `type=2` 显示“普通活动”;其余类型按上表名称展示。
+5. 两个新增名称字段为空时显示占位文案,保留原始分类值,不默认映射到其他类型。
+
+后台选项和保存方式见 [游戏类型其他选项与名称调整](游戏类型其他选项与名称调整-前端接入说明.md)。

+ 39 - 2
tests/Regression/activity_site_eligibility.php

@@ -47,7 +47,7 @@ namespace {
         'bot_user_login' => 'id INTEGER PRIMARY KEY,user_id TEXT,status INTEGER,platform TEXT',
         'bot_balance_logs' => 'id INTEGER PRIMARY KEY,member_id TEXT,type INTEGER,change_type TEXT,amount TEXT,created_at TEXT,updated_at TEXT,remark TEXT,before_balance TEXT,after_balance TEXT,related_id INTEGER,frozen_status INTEGER',
         'bot_recharge_channel_group' => 'id INTEGER PRIMARY KEY,activity_type TEXT',
-        'bot_activity_rewards' => 'id INTEGER PRIMARY KEY,title TEXT,sub_title TEXT,start_time INTEGER,end_time INTEGER,detail_image TEXT,type INTEGER,lang TEXT,pc_image TEXT,detail_banner TEXT,pc_banner TEXT,publish_platforms TEXT,budget TEXT,budget_used TEXT DEFAULT "0",quota_enabled INTEGER DEFAULT 0,reward_quota INTEGER,rewarded_count INTEGER DEFAULT 0,sort INTEGER DEFAULT 0,register_time_limited INTEGER,register_start_time TEXT,register_end_time TEXT,register_platforms TEXT,deposit_limited INTEGER,deposit_min TEXT,deposit_max TEXT,show_ineligible INTEGER DEFAULT 1,status INTEGER,params TEXT,created_at TEXT,updated_at TEXT',
+        'bot_activity_rewards' => 'id INTEGER PRIMARY KEY,title TEXT,sub_title TEXT,start_time INTEGER,end_time INTEGER,detail_image TEXT,type INTEGER,lang TEXT,game_category TEXT,pc_image TEXT,detail_banner TEXT,pc_banner TEXT,publish_platforms TEXT,budget TEXT,budget_used TEXT DEFAULT "0",quota_enabled INTEGER DEFAULT 0,reward_quota INTEGER,rewarded_count INTEGER DEFAULT 0,sort INTEGER DEFAULT 0,register_time_limited INTEGER,register_start_time TEXT,register_end_time TEXT,register_platforms TEXT,deposit_limited INTEGER,deposit_min TEXT,deposit_max TEXT,show_ineligible INTEGER DEFAULT 1,status INTEGER,params TEXT,created_at TEXT,updated_at TEXT',
         'bot_activity_reward_recipients' => 'id INTEGER PRIMARY KEY,activity_id INTEGER NOT NULL,member_id TEXT NOT NULL,created_at TEXT,UNIQUE(activity_id,member_id)',
         'bot_wallets' => 'id INTEGER PRIMARY KEY,member_id TEXT,available_balance TEXT,frozen_balance TEXT,yuebao TEXT,yuebao_invest TEXT',
         'bot_yuebao_item' => 'id INTEGER PRIMARY KEY,user_id TEXT,activity_id INTEGER,principal TEXT,surplus_principal TEXT,status INTEGER DEFAULT 1,daily_interest_rate TEXT,expire_time INTEGER,create_time INTEGER DEFAULT 0,update_time INTEGER',
@@ -81,7 +81,7 @@ namespace {
     activity(1, ['sort' => 4, 'deposit_limited' => 1, 'deposit_min' => '199.99', 'deposit_max' => '199.99', 'register_platforms' => '["APP"]']);
     activity(2, ['sort' => 9, 'publish_platforms' => '["H5"]']);
     activity(3, ['sort' => 8, 'show_ineligible' => 0, 'deposit_limited' => 1, 'deposit_min' => '300', 'deposit_max' => '400']);
-    activity(4, ['sort' => 7, 'show_ineligible' => 1, 'register_platforms' => '["PC"]']);
+    activity(4, ['sort' => 7, 'show_ineligible' => 1, 'register_platforms' => '["PC"]', 'game_category' => 'third:2']);
     activity(5, ['sort' => 10, 'status' => 0]);
     activity(6, ['type' => 2, 'sort' => 100, 'publish_platforms' => '["H5"]', 'show_ineligible' => 0, 'register_platforms' => '["PC"]']);
     request()->query = ['platform' => 'PC'];
@@ -99,6 +99,43 @@ namespace {
     foreach ([2, 3, 5] as $id) { request()->query['id'] = $id; check($controller->activityInfo()['code'] === 0, 'Hidden ID detail must be denied: ' . $id); }
     request()->query['id'] = 4;
     check($controller->activityInfo()['data']['is_eligible'] === false, 'Visible ineligible detail exposes eligibility flag');
+    $gameNames = ['third:1' => '真人', 'third:2' => '电子', 'third:3' => '彩票', 'third:4' => '体育',
+        'third:5' => '电竞', 'third:6' => '捕鱼', 'third:7' => '棋牌', 'self:sport' => 'F1体育', 'self:lottery' => 'F1彩票', 'other' => '其他'];
+    $originalActivity = \think\facade\Db::table('bot_activity_rewards')->where('id', 4)->find();
+    $fundsBefore = \think\facade\Db::table('bot_balance_logs')->order('id')->select()->toArray();
+    foreach ($gameNames as $category => $name) {
+        \think\facade\Db::table('bot_activity_rewards')->where('id', 4)->update(['game_category' => $category]);
+        $catalogList = new \app\api\lists\ActivityRewardsLists(1001);
+        $catalogList->limitLength = 100;
+        $listed = array_column($catalogList->lists(), null, 'id')[4];
+        $detail = $controller->activityInfo();
+        check($detail['code'] === 1, 'Visible catalog detail succeeds: ' . $category);
+        foreach ([$listed, $detail['data']] as $item) {
+            check($item['type'] === 5 && $item['type_name'] === 'USDT使用教学', 'Activity type value and name are returned together');
+            check($item['game_category'] === $category && $item['game_category_name'] === $name, 'Category value/name are returned on list and detail: ' . $category);
+            check($item['is_eligible'] === false, 'Catalog enrichment preserves member eligibility');
+        }
+        check($catalogList->count() === 3, 'Category names do not change visibility or pagination totals');
+    }
+    \think\facade\Db::table('bot_activity_rewards')->where('id', 4)->update(['game_category' => null]);
+    $uncategorized = $controller->activityInfo()['data'];
+    check($uncategorized['game_category'] === null && $uncategorized['game_category_name'] === null, 'Unassigned category remains null, not other');
+    $defaultList = new \app\api\lists\ActivityRewardsLists(1001);
+    $defaultList->limitLength = 100;
+    $defaultRows = array_column($defaultList->lists(), null, 'id');
+    check($defaultRows[4]['game_category'] === null && $defaultRows[4]['game_category_name'] === null, 'Unassigned list category is explicit null');
+    check($defaultRows[6]['type'] === 2 && $defaultRows[6]['type_name'] === '普通活动', 'Former football type displays the normal activity name without changing its ID');
+    foreach ([2 => '普通活动', 3 => '即充即送', 4 => '老用户召集令', 5 => 'USDT使用教学', 6 => '余额宝'] as $type => $name) {
+        $named = \app\common\service\ActivityCatalog::decorate(['type' => $type, 'game_category' => 'other']);
+        check($named['type'] === $type && $named['type_name'] === $name && $named['game_category_name'] === '其他', 'Canonical activity label: ' . $type);
+    }
+    $legacyType = \app\common\service\ActivityCatalog::decorate(['type' => 1]);
+    check($legacyType['type'] === 1 && $legacyType['type_name'] === null && $legacyType['game_category'] === null, 'Legacy type one is preserved without inventing a name');
+    $unknownType = \app\common\service\ActivityCatalog::decorate(['type' => 99, 'game_category' => 'legacy:unknown']);
+    check($unknownType['type'] === 99 && $unknownType['type_name'] === null && $unknownType['game_category'] === 'legacy:unknown' && $unknownType['game_category_name'] === null, 'Unknown values remain unchanged without guessing another category');
+    \think\facade\Db::table('bot_activity_rewards')->where('id', 4)->update(['game_category' => 'third:2']);
+    check(\think\facade\Db::table('bot_activity_rewards')->where('id', 4)->find() === $originalActivity, 'List/detail metadata reads do not modify the activity');
+    check(\think\facade\Db::table('bot_balance_logs')->order('id')->select()->toArray() === $fundsBefore, 'List/detail metadata reads never create financial entries');
     activity(7, ['type' => 4]);
     request()->query['id'] = 7;
     check($controller->activityInfo()['code'] === 0, 'Detail enforces recharge group without type query');