Explorar o código

迁移命令新增 --wager-only,部署不动已有冻结金额

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
doge hai 1 día
pai
achega
d2c140bf49

+ 11 - 9
app/Console/Commands/MigrateFrozenRewards.php

@@ -22,7 +22,7 @@ class MigrateFrozenRewards extends Command
 {
     private const LEGACY_TYPES = ['即充即送', '老用户回归'];
 
-    protected $signature = 'wagering:migrate-frozen-rewards {--dry-run : 只统计不写入} {--member= : 只处理指定会员ID}';
+    protected $signature = 'wagering:migrate-frozen-rewards {--dry-run : 只统计不写入} {--member= : 只处理指定会员ID} {--wager-only : 不动冻结金额,只把进行中申请活动的剩余打码量计入统一打码量}';
     protected $description = '把冻结中的活动奖励转入可用余额,并把未完成流水计入提现打码量';
 
     /** @var array<int,true> 已匹配过的 wallet_bonus,避免同一笔充值的两条同额奖励共用一条流水要求 */
@@ -38,9 +38,10 @@ class MigrateFrozenRewards extends Command
             return self::FAILURE;
         }
         $dryRun = (bool)$this->option('dry-run');
-        foreach ($this->members() as $memberId) {
+        $wagerOnly = (bool)$this->option('wager-only');
+        foreach ($this->members($wagerOnly) as $memberId) {
             try {
-                DB::transaction(fn () => $this->migrateMember((string)$memberId, $dryRun), 3);
+                DB::transaction(fn () => $this->migrateMember((string)$memberId, $dryRun, $wagerOnly), 3);
             } catch (\Throwable $e) {
                 ++$this->stats['skipped'];
                 $this->warn("会员 {$memberId} 迁移失败:" . $e->getMessage());
@@ -53,11 +54,11 @@ class MigrateFrozenRewards extends Command
     }
 
     /** @return string[] */
-    private function members(): array
+    private function members(bool $wagerOnly): array
     {
         if ($this->option('member')) return [(string)$this->option('member')];
-        $ids = BalanceLog::where('type', 2)->where('frozen_status', 1)->whereIn('change_type', self::LEGACY_TYPES)->distinct()->pluck('member_id')->all();
-        if (Schema::hasTable('promotion_rewards')) {
+        $ids = $wagerOnly ? [] : BalanceLog::where('type', 2)->where('frozen_status', 1)->whereIn('change_type', self::LEGACY_TYPES)->distinct()->pluck('member_id')->all();
+        if (!$wagerOnly && Schema::hasTable('promotion_rewards')) {
             $ids = array_merge($ids, DB::table('promotion_rewards as r')->join('users as u', 'u.id', '=', 'r.user_id')
                 ->where('r.status', 'locked')->distinct()->pluck('u.member_id')->all());
         }
@@ -67,7 +68,7 @@ class MigrateFrozenRewards extends Command
         return array_values(array_unique(array_map('strval', $ids)));
     }
 
-    private function migrateMember(string $memberId, bool $dryRun): void
+    private function migrateMember(string $memberId, bool $dryRun, bool $wagerOnly = false): void
     {
         $wallet = Wallet::where('member_id', $memberId)->lockForUpdate()->first();
         if (!$wallet) throw new \RuntimeException('钱包不存在');
@@ -75,8 +76,9 @@ class MigrateFrozenRewards extends Command
         $now = PromotionFacts::now();
         $moved = '0'; $wager = '0'; $touched = false;
 
+        // --wager-only:旧冻结奖励留在冻结余额,继续由原有解冻逻辑处理
         // 1) 旧版冻结返彩
-        foreach (BalanceLog::where('member_id', $memberId)->where('type', 2)->where('frozen_status', 1)
+        if (!$wagerOnly) foreach (BalanceLog::where('member_id', $memberId)->where('type', 2)->where('frozen_status', 1)
                      ->whereIn('change_type', self::LEGACY_TYPES)->orderBy('id')->lockForUpdate()->get() as $log) {
             $key = 'balance_log:' . $log->id;
             if ($this->done($memberId, $key)) continue;
@@ -103,7 +105,7 @@ class MigrateFrozenRewards extends Command
         }
 
         // 2) 新活动 locked 奖励
-        if ($user && Schema::hasTable('promotion_rewards')) {
+        if (!$wagerOnly && $user && Schema::hasTable('promotion_rewards')) {
             $facts = new PromotionFacts();
             foreach (DB::table('promotion_rewards')->where('user_id', $user->id)->where('status', 'locked')->orderBy('id')->lockForUpdate()->get() as $reward) {
                 $key = 'promotion_reward:' . $reward->id;

+ 7 - 3
docs/后端/提现打码量与冻结金额-部署说明.md

@@ -8,13 +8,15 @@
 2. 部署 bot-28 代码。
 3. 部署 melbet_sport-api。代码会检查钱包是否已有新字段,迁移前保持旧逻辑。
 4. 首次计算:执行一次 `php artisan wagering:sync --all`,之后由定时任务每分钟执行 `wagering:sync`。
-5. 迁移冻结奖励,先 dry-run:
+5. 迁移进行中申请活动的剩余打码量(**不动冻结金额**),先 dry-run:
    ```bash
-   php artisan wagering:migrate-frozen-rewards --dry-run -v
+   php artisan wagering:migrate-frozen-rewards --wager-only --dry-run -v
    ```
    ```bash
-   php artisan wagering:migrate-frozen-rewards
+   php artisan wagering:migrate-frozen-rewards --wager-only
    ```
+   上线前已冻结的活动奖励(即充即送、老用户回归、`locked` 的新活动奖励、余额宝利息)保留在冻结余额,继续由原有逻辑解冻:bot-28 的 `promotions:sync` 解锁新活动奖励,melbet 下注时按1倍流水解冻旧返彩。上线后的新奖励不再进入冻结余额。
+   如果以后想把这些旧冻结奖励一次性转入可用余额,再去掉 `--wager-only` 执行(见下文“迁移命令说明”);不执行也不影响本次功能。
 6. 在后台角色设置中分配新按钮:`admin/walletFreeze`、`admin/user/freeze|unfreeze`、`admin/manualAudit/freeze|unfreeze`。
 
 ## 行为变化
@@ -45,6 +47,8 @@
 
 ## 迁移命令说明
 
+`--wager-only` 只执行下面第三项(进行中的申请活动),不改任何余额和资金日志。不加该选项时执行全部三项:
+
 `wagering:migrate-frozen-rewards`:
 
 - 仍冻结的 即充即送 / 老用户回归 日志(`type=2, frozen_status=1`):转入可用余额,并成对写 `流水解冻` 日志。增加的打码量为对应 `wallet_bonus.need_flow`(同一笔充值的同额奖励各自匹配一条),没有则按奖金1倍,再减去会员旧的 `wallets.flow_balance` 结余,结余随后归 0。

+ 19 - 0
tests/Integration/WageringTest.php

@@ -291,6 +291,25 @@ class WageringTest extends TestCase
         $this->assertSame('80.00', $this->money($this->wallet()->wager_completed));
     }
 
+    public function test_wager_only_migration_never_touches_frozen_balance(): void
+    {
+        DB::table('wallets')->where('member_id', 'm1')->update(['frozen_balance' => '10']);
+        DB::table('balance_logs')->insert(['member_id' => 'm1', 'type' => 2, 'change_type' => '即充即送', 'amount' => '10', 'related_id' => 7, 'frozen_status' => 1]);
+        DB::connection()->getSchemaBuilder()->create('activity_user', function (Blueprint $t) {
+            $t->id(); $t->string('member_id'); $t->integer('status'); $t->decimal('betting_amount', 20, 2); $t->decimal('effective_betting_amount', 20, 2);
+        });
+        DB::table('activity_user')->insert(['member_id' => 'm1', 'status' => \App\Models\ActivityUser::STATUS_IN_PROGRESS, 'betting_amount' => '20', 'effective_betting_amount' => '5']);
+        $command = new MigrateFrozenRewards();
+        $command->setLaravel($this->app);
+        $command->run(new ArrayInput(['--wager-only' => true]), new BufferedOutput());
+        $wallet = $this->wallet();
+        $this->assertSame('10.00', $this->money($wallet->frozen_balance));
+        $this->assertSame('100.00', $this->money($wallet->available_balance));
+        $this->assertSame(1, DB::table('balance_logs')->where('type', 2)->where('frozen_status', 1)->count());
+        $this->assertSame(0, DB::table('balance_logs')->where('change_type', '流水解冻')->count());
+        $this->assertSame('15.00', $this->money($wallet->wager_required));
+    }
+
     public function test_one_time_migration_moves_frozen_rewards_to_available_and_carries_their_wagering(): void
     {
         DB::table('wallets')->where('member_id', 'm1')->update(['frozen_balance' => '25', 'flow_balance' => '4']);