瀏覽代碼

禁止投注

doge 1 天之前
父節點
當前提交
4251891754

+ 7 - 6
app/Services/PromotionService.php

@@ -440,7 +440,7 @@ class PromotionService
             if (!$activityId && FirstDepositChoice::ready() && !FirstDepositChoice::legacyOrder($orderSource, (int) $deposit->related_id)) return;
 
             // 多个首充活动按档位发放:在所有可参加的首充活动里,取充值金额达到门槛、奖励最高的一个(份数或额度用完时取下一档)。
-            // 会员确认的禁止游戏大类(全部首充活动统一)沿用确认时的快照;选中的那个活动沿用确认时的完整规则。
+            // 会员确认的那个活动沿用确认时的奖励和打码倍数;禁止的游戏大类以后台当前设置为准(下注时实时读)。
             // 只在会员确认时所在的端(PC/H5/APP/TG)发布的活动里选;上线前的选择没有记录端,按会员确认的那个活动发布的端。
             $snapshot = $activityId ? $this->json($choice->rule_snapshot) : null;
             $platforms = isset($snapshot['platform']) ? [$snapshot['platform']] : [];
@@ -464,9 +464,10 @@ class PromotionService
                 if (!$published || $creditedAt < $published || !ActivityPeriod::contains($activity->start_time, $activity->end_time, strtotime($creditedAt))) continue;
                 if (!ActivityEligibilityService::evaluate($activity, ActivityEligibilityService::memberContext($member->member_id))['is_eligible']) continue;
                 // Pre-cutover orders keep their legacy reward without new restrictions.
+                // 禁止的游戏大类在下注时实时读后台设置,这里的快照只作记录。
                 if ($snapshot === null) $rules = array_replace($this->rules($activity), ['forbidden_categories' => []]);
-                elseif ((int)$activity->id === $activityId) $rules = $snapshot;
-                else $rules = array_replace($this->rules($activity), ['forbidden_categories' => $snapshot['forbidden_categories'] ?? []]);
+                elseif ((int)$activity->id === $activityId) $rules = array_replace($snapshot, ['forbidden_categories' => Rules::forbiddenSetting()]);
+                else $rules = $this->rules($activity);
                 if (!$rules || bccomp((string)$deposit->amount, (string)$rules['min_deposit'], 10) < 0) continue;
                 $candidates[] = [$activity, $rules];
             }
@@ -490,10 +491,10 @@ class PromotionService
         $rewards = DB::table('promotion_rewards')->where('user_id', $userId)->whereIn('status', ['paid', 'locked'])
             ->where('required_flow', '>', 0)->sharedLock()->get();
         $categories = array_keys(ShareDefaults::commissionCategories());
+        // 禁止的游戏大类实时读后台当前设置,不用发奖时存下的快照。
+        $forbidden = array_values(array_intersect($categories, Rules::forbiddenSetting()));
+        if (!in_array('self:lottery', $forbidden, true) || count($forbidden) === count($categories)) return false;
         foreach ($rewards as $reward) {
-            $configured = (array) ($this->json($reward->rule_snapshot)['forbidden_categories'] ?? []);
-            $forbidden = array_intersect($categories, array_filter($configured, 'is_string'));
-            if (!in_array('self:lottery', $forbidden, true) || count($forbidden) === count($categories)) continue;
             $activity = DB::table('promotion_activities')->where('id', $reward->activity_id)->where('type', Rules::FIRST_DEPOSIT)
                 ->where('status', 1)->sharedLock()->first(['start_time', 'end_time']);
             if (!$activity || !ActivityPeriod::contains($activity->start_time, $activity->end_time, now()->timestamp)) continue;

+ 4 - 0
docs/前端交付/README.md

@@ -2,6 +2,10 @@
 
 本目录是2026-10-26的接口文档交付副本,按**项目 → 前台/后台/代理端**组织。
 
+## 首充禁止游戏实时生效(2026-11-13)
+
+- [首充禁止游戏实时生效-修改单](首充禁止游戏实时生效-修改单.md):禁止投注的游戏大类改为下注时实时读后台当前设置,后台修改后对已领奖、打码量未完成的会员立即生效。接口不变,前端不用改。
+
 ## 首充活动详情链接(2026-11-13)
 
 - [首充活动详情链接-前端修改单](首充活动详情链接-前端修改单.md):总后台新增 `GET/POST /admin/promotion/first-deposit-links`,PC、H5、APP 各填一个活动详情链接;会员前台 `GET /api/promotion/firstDeposit` 新增 `detail_url`,按请求的 `platform` 自动返回对应链接。

+ 1 - 1
docs/前端交付/首充活动返回多个-前端修改单.md

@@ -79,7 +79,7 @@
 - 打码倍数按实际发放的那个活动计算。
 - 高档的份数或额度用完时,发下一档。
 - 只在会员确认时所在的端(PC / H5 / APP)发布的活动里选。只发布在 PC 的活动,H5 上确认的会员拿不到。
-- 禁止投注的游戏大类对全部首充活动统一,按会员确认时的设置。
+- 禁止投注的游戏大类对全部首充活动统一,实时按后台当前设置。
 
 ## 前端要改的
 

+ 40 - 0
docs/前端交付/首充禁止游戏实时生效-修改单.md

@@ -0,0 +1,40 @@
+# 首充禁止投注游戏大类改为实时生效-修改单(2026-11-13)
+
+## 改动前
+
+会员确认参加首充时,后端会把当时的「禁止投注的游戏大类」存下来,发奖后按存下来的设置限制。所以后台**后来**新增的禁止大类,对已经参加的会员不生效,会员领到奖励后仍然可以投注这些游戏。
+
+## 改动后
+
+禁止投注的游戏大类**每次下注时实时读后台当前设置**:
+
+- 后台新增禁止大类:已经领到首充奖励、打码量还没完成的会员,**立即**不能投注新增的大类。
+- 后台去掉某个大类:这些会员立即可以投注这个大类。
+- 后台清空设置:首充奖励不再限制任何游戏。
+
+其他规则不变:
+- 限制所有**领到了首充奖励**的会员,包括以前(首充改为需要勾选参加之前)领到首充奖励的会员。没领到首充奖励的会员不受限制。
+- 限制从发奖开始,到打码量完成为止;活动下架或结束后也不再限制。
+- 打码倍数为 0 的奖励没有打码量,不限制。
+- 会员确认参加的那个活动,奖励金额和打码倍数仍按确认时的条款。
+
+## 接口
+
+**接口和字段都不变**,前端不用改。
+
+- 总后台设置仍然是 `GET/POST /admin/promotion/first-deposit-restriction`,保存后立即生效。
+- 会员前台 `GET /api/promotion/firstDeposit` 返回的 `restrictions[].forbidden_categories` 现在显示后台**当前**的设置。
+- 下注被拦截时的返回不变:`code=11003`,`msg=活动期间禁止投注该游戏`。
+
+## 涉及的拦截点
+
+- 会员前台(melbet):自营彩票、自营体育的下注接口,以及进入三方游戏(`/api/egame/gameUrl`)。
+- Telegram:F1 彩票下注。
+
+## 验收
+
+1. 会员参加首充并领到奖励(打码倍数大于 0),此时后台没有设置禁止大类,会员可以投注 F1 彩票。
+2. 后台把 F1 彩票设为禁止投注并保存,这个会员再投注 F1 彩票,提示「活动期间禁止投注该游戏」。
+3. 后台去掉 F1 彩票,会员又可以投注。
+4. 以前领到首充奖励、打码量还没完成的老会员,同样按当前设置限制。
+5. 没有领到首充奖励的会员,不管后台怎么设置都不受限制。

+ 1 - 1
docs/前端接口/独立活动/首充活动参加与禁止游戏-会员接入.md

@@ -133,7 +133,7 @@
 - 以首笔充值到账前最后一次保存的选择为准。切换前已下单且没有选择记录的充值按原有首充规则处理。
 - 首笔充值到账后不能再修改选择。
 - 首笔充值时不参加,第二笔及以后的充值都不再算首充,不会再有首充奖励。
-- 有多个首充活动时按档位发放:首笔充值到账时,在会员确认时所在端发布的首充活动里,取充值金额达到门槛、奖励最高的一个(份数或额度用完时取下一档)。会员确认的那个活动按确认时的条款发放;禁止游戏大类按确认时的设置。活动需有效并有可用奖励额度。
+- 有多个首充活动时按档位发放:首笔充值到账时,在会员确认时所在端发布的首充活动里,取充值金额达到门槛、奖励最高的一个(份数或额度用完时取下一档)。会员确认的那个活动按确认时的奖励和打码倍数发放;禁止游戏大类实时按后台当前设置,领奖后后台修改也立即生效。活动需有效并有可用奖励额度。
 - Telegram 会员可在充值菜单或用 `/firstdeposit` 确认选择。
 
 ## 3. 下注和进入游戏被拒绝

+ 1 - 1
docs/总后台/独立活动/首充奖励-禁止游戏大类-后台接入.md

@@ -48,7 +48,7 @@
 
 ## 生效说明
 
-- 设置对全部首充活动生效。会员确认参加时记录当时的设置,修改后只影响之后确认参加的会员;已确认或已发奖的会员按确认时的设置限制。
+- 设置对全部首充活动生效,**实时生效**:下注时读后台当前设置。修改后,已经领到首充奖励、打码量还没完成的会员也立即按新设置限制。
 - 修改设置后,会员端已打开的首充页面需要刷新后重新确认参加。
 - 首充活动的其余规则(`min_deposit/reward_amount/turnover_ratio` 等)仍在每条首充奖励里配置,接口不变。
 - Telegram 机器人只展示能在机器人内完成打码的首充活动:禁止了 `F1彩票` 且奖励有打码量时,机器人内不提供首充活动。

+ 2 - 0
tests/Integration/LotteryBetSourceTest.php

@@ -289,6 +289,8 @@ class LotteryBetSourceTest extends TestCase
         DB::table('promotion_rewards')->insert(['activity_id' => $activity, 'user_id' => 1, 'account_id' => 1, 'round_no' => 1,
             'amount' => '25', 'required_flow' => '50', 'status' => 'paid', 'rule_snapshot' => '{"forbidden_categories":["self:lottery"]}',
             'created_at' => $now]);
+        // 禁止大类实时读后台设置(没有参加记录的旧会员也一样)。
+        DB::table('config')->updateOrInsert(['field' => \App\Support\PromotionRules::FORBIDDEN_CONFIG], ['val' => '["self:lottery"]']);
 
         $this->assertSame('活动期间禁止投注该游戏', BetService::bet('1001', '大10')['text']);
         $this->assertSame(0, DB::table('lhc_order')->count());

+ 8 - 2
tests/Integration/PromotionActivitiesTest.php

@@ -1002,8 +1002,14 @@ class PromotionActivitiesTest extends TestCase
         $this->restrict(['third:5']);
         $this->service->onDeposit($this->deposit(1, '100'));
         $snapshot = json_decode(DB::table('promotion_rewards')->where('user_id', 1)->value('rule_snapshot'), true);
-        $this->assertSame(['third:1', 'third:2', 'self:sport'], $snapshot['forbidden_categories']);
+        $this->assertSame(['third:5'], $snapshot['forbidden_categories'], 'records the setting at credit time');
         foreach ($this->service->listing(2, 'H5', 'zh', 8)['data'] as $item) $this->assertSame(['third:5'], $item['rules']['forbidden_categories']);
+        // Telegram 下注时实时读当前设置:领奖后再加上 F1彩票,立即受限;去掉又恢复。
+        $this->assertFalse($this->service->lotteryRestricted('m1'));
+        $this->restrict(['self:lottery']);
+        $this->assertTrue($this->service->lotteryRestricted('m1'));
+        $this->restrict(['third:5']);
+        $this->assertFalse($this->service->lotteryRestricted('m1'));
     }
 
     public function test_first_deposit_pays_the_highest_tier_the_deposit_reaches(): void
@@ -1069,7 +1075,7 @@ class PromotionActivitiesTest extends TestCase
         $reward = DB::table('promotion_rewards')->where('user_id', 1)->first();
         $this->assertSame($h5, (int) $reward->activity_id);
         $this->assertSame('25.00', json_decode($reward->rule_snapshot, true)['reward_amount']);
-        $this->assertSame([], json_decode($reward->rule_snapshot, true)['forbidden_categories']);
+        $this->assertSame(['self:sport'], json_decode($reward->rule_snapshot, true)['forbidden_categories'], 'current setting, not the one shown at confirmation');
     }
 
     public function test_pending_order_cutover_is_source_specific_and_keeps_legacy_terms(): void