Kaynağa Gözat

测试结算

doge 19 saat önce
ebeveyn
işleme
a863930ce5

+ 8 - 2
app/Console/Commands/SettleSharing.php

@@ -9,13 +9,19 @@ use Illuminate\Console\Command;
 
 class SettleSharing extends Command
 {
-    protected $signature='share:settle {date? : 原下注日期Y-m-d,默认北京时间昨日} {--preview : 只读预览,不登记应付佣金} {--reconcile : 处理迟到结算和改判}';
+    protected $signature='share:settle {date? : 原下注日期Y-m-d,默认北京时间昨日;allow-today时默认当天} {--preview : 只读预览,不登记应付佣金} {--reconcile : 处理迟到结算和改判} {--allow-today : 允许调试结算北京时间当天,不允许未来日期}';
     protected $description='每日结算新分享应付佣金,不直接入会员钱包';
     public function handle(ShareSettlementService $service): int
     {
         try {
             if ($this->option('reconcile') && $this->option('preview')) throw new \InvalidArgumentException('reconcile不支持preview,请使用日期预览');
-            $result=$this->option('reconcile') ? $service->reconcile() : $service->settle($this->argument('date') ?: Carbon::now(ShareDefaults::TIMEZONE)->subDay()->toDateString(),!(bool)$this->option('preview'));
+            $allowToday=(bool)$this->option('allow-today');
+            if ($this->option('reconcile') && $allowToday) throw new \InvalidArgumentException('allow-today不能与reconcile同时使用');
+            $defaultDate=Carbon::now(ShareDefaults::TIMEZONE);
+            if (!$allowToday) $defaultDate->subDay();
+            $result=$this->option('reconcile') ? $service->reconcile() : $service->settle(
+                $this->argument('date') ?: $defaultDate->toDateString(), !(bool)$this->option('preview'), $allowToday
+            );
             $this->line(json_encode($result,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR)); return self::SUCCESS;
         } catch (\Throwable $e) { $this->error($e->getMessage()); return self::FAILURE; }
     }

+ 8 - 4
app/Services/Sharing/ShareSettlementService.php

@@ -115,16 +115,18 @@ class ShareSettlementService
         return $result;
     }
 
-    public function settle(string $date,bool $write=true): array
+    public function settle(string $date,bool $write=true,bool $allowToday=false): array
     {
         $this->validateDate($date);
-        if ($date>=Carbon::now(ShareDefaults::TIMEZONE)->toDateString()) throw new \InvalidArgumentException('当天及未来佣金只能预估,不能结算到账本');
+        $today=Carbon::now(ShareDefaults::TIMEZONE)->toDateString();
+        if ($date>$today) throw new \InvalidArgumentException('未来日期佣金不能结算到账本');
+        if ($date===$today && !$allowToday) throw new \InvalidArgumentException('当天佣金只能预估,调试结算请显式使用--allow-today');
         $activation=DB::table('share_settings')->value('activation_date');
         $result=['date'=>$date,'receivers'=>0,'changed'=>0,'delta'=>'0.0000000000','written'=>$write];
         if (!$activation || $date<$activation) return $result;
         if (!DB::table('share_system_state')->where('key','initialized_at')->exists()) throw new \RuntimeException('分享关系尚未初始化');
         // Facts, membership closure and policy must not change in the middle of this projection.
-        return DB::transaction(function () use ($date,$write,$result) {
+        return DB::transaction(function () use ($date,$write,$result,$today) {
             DB::table('share_system_state')->where('key','relations_lock')->lockForUpdate()->first();
             $projection=$this->projection($date); $rows=$projection['rows']; $policy=$projection['policy'];
             $depthRates=self::usesDepthRates($date);
@@ -167,10 +169,12 @@ class ShareSettlementService
                 }
                 if ($write && $account) $this->upgrade((int)$userId,(string)$account->earned_amount,$now);
             }
-            if ($write) {
+            if ($write && $date<$today) {
                 $last=DB::table('share_system_state')->where('key','last_settlement_date')->value('value');
                 if (!$last || $date>$last) DB::table('share_system_state')->updateOrInsert(['key'=>'last_settlement_date'],['value'=>$date,'created_at'=>$now,'updated_at'=>$now]);
             }
+            // A debug credit is still an open day: keep it pending for normal reconciliation after midnight.
+            if ($write && $date===$today) $this->requeue([$date]);
             return $result;
         });
     }

+ 42 - 0
docs/后端/分享返佣-当天调试结算.md

@@ -0,0 +1,42 @@
+# 分享返佣:当天调试结算
+
+在服务器 `bot-28` 项目目录中执行。
+
+## 结算北京时间当天
+
+```bash
+php artisan share:sync-facts --write
+php artisan share:settle --allow-today
+```
+
+`--allow-today` 是显式调试参数:不传日期时改为结算北京时间当天。命令本身不自动同步投注明细,所以需要更新数据时先执行 `share:sync-facts --write`。
+
+也可以指定日期,例如在北京时间 `2026-10-25` 当天执行:
+
+```bash
+php artisan share:settle 2026-10-25 --allow-today
+```
+
+只读预览当天结果:
+
+```bash
+php artisan share:settle --allow-today --preview
+```
+
+## 参数与实际效果
+
+| 用法 | 结果 |
+| --- | --- |
+| `share:settle` | 保持默认结算北京时间昨天 |
+| `share:settle YYYY-MM-DD` | 结算指定已结束日期,默认仍拒绝当天和未来日期 |
+| `share:settle --allow-today` | 调试结算北京时间当天 |
+| `share:settle YYYY-MM-DD --allow-today` | 允许指定当天或历史日期,仍拒绝未来日期 |
+| `share:settle --allow-today --preview` | 只读预览当天,不写佣金账户、账本、等级或日结标记 |
+
+`--allow-today` 不能与 `--reconcile` 同时使用。自动调度没有添加该参数,仍只处理已结束的日期。
+
+当天调试结算按已同步、已结算且有效的投注明细计算,会写入**真实应付佣金账户**,可能成为会员可申请金额;不会直接转入会员钱包,钱包入账仍需领取申请和后台审核。
+
+同一天反复执行按现有账本计算差额,不会把相同佣金重复累计。新增有效投注、迟到结算或改判后,再次执行会按最新明细修正差额。
+
+当天调试不会把全局“最近完成日结日期”提前标记为当天。该日期保留在待核对队列中,跨日后的正常日结或补结算会继续处理,已有调试入账金额只补差额。

+ 108 - 1
tests/Integration/SharingSettlementTest.php

@@ -2,6 +2,7 @@
 
 namespace Tests\Integration;
 
+use App\Console\Commands\SettleSharing;
 use App\Services\Sharing\ShareDefaults;
 use App\Services\Sharing\ShareRelations;
 use App\Services\Sharing\ShareSettlementService;
@@ -13,6 +14,7 @@ use Illuminate\Foundation\Application;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Facade;
 use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Tester\CommandTester;
 
 class SharingSettlementTest extends TestCase
 {
@@ -289,13 +291,118 @@ class SharingSettlementTest extends TestCase
         $this->service->settle('2026-10-24'); $this->assertSame('0.2000000000',$this->amount(1));
     }
 
-    public function test_today_never_becomes_claimable_and_foreign_currency_is_not_combined(): void
+    public function test_today_is_rejected_without_debug_opt_in_and_foreign_currency_is_not_combined(): void
     {
         $this->fact(2,'200'); $this->fact(3,'20000','2026-10-24',['currency'=>'USD']);
         $this->assertSame('0',$this->service->projection('2026-10-24')['rows'][1]['third:2']['indirect_performance']);
         $this->expectException(\InvalidArgumentException::class); $this->service->settle('2026-10-25');
     }
 
+    private function command(array $options): CommandTester
+    {
+        $command = new SettleSharing();
+        $command->setLaravel(app());
+        $tester = new CommandTester($command);
+        $tester->execute($options);
+        return $tester;
+    }
+
+    public function test_debug_today_credits_once_revises_only_the_delta_and_closes_normally_after_midnight(): void
+    {
+        $this->service->settle('2026-10-24');
+        $fact = $this->fact(2, '200', '2026-10-25');
+        $wallets = DB::table('wallets')->get()->toJson();
+        $first = $this->service->settle('2026-10-25', true, true);
+        $this->assertTrue($first['written']);
+        $this->assertSame('0.2000000000', $this->amount(1));
+        $this->assertSame('2026-10-24', DB::table('share_system_state')->where('key', 'last_settlement_date')->value('value'));
+        $this->assertContains('2026-10-25', json_decode(DB::table('share_system_state')->where('key', 'facts_dirty_dates')->value('value'), true));
+        $entries = DB::table('share_ledger')->count();
+        $this->assertSame(0, $this->service->settle('2026-10-25', true, true)['changed']);
+        $this->assertSame($entries, DB::table('share_ledger')->count());
+        DB::table('share_bet_facts')->where('id', $fact)->update(['effective_amount' => 400]);
+        $this->assertSame('0.2000000000', $this->service->settle('2026-10-25', true, true)['delta']);
+        $this->assertSame('0.4000000000', $this->amount(1));
+        $this->assertSame(0, $this->service->reconcile()['dates']);
+        Carbon::setTestNow(Carbon::parse('2026-10-26 00:01:00', 'Asia/Shanghai'));
+        $entries = DB::table('share_ledger')->count();
+        $this->assertSame(1, $this->service->reconcile()['dates']);
+        $this->assertSame('2026-10-25', DB::table('share_system_state')->where('key', 'last_settlement_date')->value('value'));
+        $this->assertSame($entries, DB::table('share_ledger')->count());
+        $this->assertSame('0.4000000000', $this->amount(1));
+        $this->assertSame($wallets, DB::table('wallets')->get()->toJson());
+    }
+
+    public function test_allow_today_flag_defaults_to_today_and_ordinary_command_still_defaults_to_yesterday(): void
+    {
+        $this->fact(2, '200'); $this->fact(2, '300', '2026-10-25');
+        $normal = $this->command([]);
+        $this->assertSame(0, $normal->getStatusCode());
+        $this->assertSame('2026-10-24', json_decode($normal->getDisplay(), true)['date']);
+        $this->assertSame('0.2000000000', $this->amount(1));
+        $blocked = $this->command(['date' => '2026-10-25']);
+        $this->assertSame(1, $blocked->getStatusCode());
+        $this->assertStringContainsString('--allow-today', $blocked->getDisplay());
+        $debug = $this->command(['--allow-today' => true]);
+        $this->assertSame(0, $debug->getStatusCode());
+        $this->assertSame('2026-10-25', json_decode($debug->getDisplay(), true)['date']);
+        $this->assertSame('0.5000000000', $this->amount(1));
+        $again = $this->command(['date' => '2026-10-25', '--allow-today' => true]);
+        $this->assertSame(0, $again->getStatusCode());
+        $this->assertSame(0, json_decode($again->getDisplay(), true)['changed']);
+        $this->assertSame('2026-10-24', DB::table('share_system_state')->where('key', 'last_settlement_date')->value('value'));
+    }
+
+    public function test_debug_preview_today_is_read_only_including_levels_markers_and_reconciliation_queue(): void
+    {
+        $this->fact(2, '50000', '2026-10-25');
+        DB::table('share_levels')->insert(['level' => 2, 'name' => '二级', 'incremental_commission' => 100]);
+        $tables = ['share_accounts', 'share_settlements', 'share_ledger', 'share_members', 'share_system_state', 'wallets'];
+        $before = [];
+        foreach ($tables as $table) $before[$table] = DB::table($table)->get()->toJson();
+        $preview = $this->command(['--allow-today' => true, '--preview' => true]);
+        $this->assertSame(0, $preview->getStatusCode());
+        $data = json_decode($preview->getDisplay(), true);
+        $this->assertSame('2026-10-25', $data['date']);
+        $this->assertFalse($data['written']);
+        $this->assertGreaterThan(0, $data['changed']);
+        foreach ($tables as $table) $this->assertSame($before[$table], DB::table($table)->get()->toJson(), $table);
+    }
+
+    /** @dataProvider forbiddenDebugOptions */
+    public function test_future_dates_and_incompatible_debug_options_are_rejected_before_writing(array $options): void
+    {
+        $this->fact(2, '200', '2026-10-25');
+        $before = DB::table('share_system_state')->get()->toJson();
+        $command = $this->command($options);
+        $this->assertSame(1, $command->getStatusCode());
+        $this->assertSame(0, DB::table('share_ledger')->count());
+        $this->assertSame(0, DB::table('share_settlements')->count());
+        $this->assertSame('0.0000000000', $this->amount(1));
+        $this->assertSame($before, DB::table('share_system_state')->get()->toJson());
+    }
+
+    public static function forbiddenDebugOptions(): array
+    {
+        return [
+            'future' => [['date' => '2026-10-26']],
+            'future with opt in' => [['date' => '2026-10-26', '--allow-today' => true]],
+            'future preview' => [['date' => '2026-10-26', '--allow-today' => true, '--preview' => true]],
+            'invalid calendar date' => [['date' => '2026-02-30', '--allow-today' => true]],
+            'reconcile' => [['--allow-today' => true, '--reconcile' => true]],
+            'reconcile and preview' => [['--allow-today' => true, '--reconcile' => true, '--preview' => true]],
+        ];
+    }
+
+    public function test_debug_today_uses_beijing_date_even_when_utc_is_still_yesterday(): void
+    {
+        Carbon::setTestNow(Carbon::parse('2026-10-25 00:01:00', 'Asia/Shanghai'));
+        $command = $this->command(['--allow-today' => true]);
+        $this->assertSame(0, $command->getStatusCode());
+        $this->assertSame('2026-10-25', json_decode($command->getDisplay(), true)['date']);
+        $this->assertFalse(DB::table('share_system_state')->where('key', 'last_settlement_date')->exists());
+    }
+
     public function test_open_day_stays_queued_and_a_missed_midnight_run_is_automatically_recovered(): void
     {
         Carbon::setTestNow(Carbon::parse('2026-10-24 12:00:00','Asia/Shanghai'));