Răsfoiți Sursa

全部日期

doge 2 zile în urmă
părinte
comite
be59da34e1

+ 58 - 9
app/Console/Commands/BackfillShareCommissions.php

@@ -3,9 +3,11 @@
 namespace App\Console\Commands;
 
 use App\Models\BalanceLog;
+use App\Services\ShareCommissionDiagnostics;
 use App\Services\ShareCommissionRecorder;
 use Carbon\Carbon;
 use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
 
 class BackfillShareCommissions extends Command
 {
@@ -13,6 +15,8 @@ class BackfillShareCommissions extends Command
         {--start-date= : 返现入账起始日 Y-m-d,默认结束日前30天}
         {--end-date= : 返现入账结束日 Y-m-d,默认今天}
         {--chunk=500 : 每批读取条数,1至2000}
+        {--all : 查询全部历史,不能与日期选项同时使用}
+        {--diagnose : 只读诊断数据来源与各级筛选,不登记或发奖}
         {--write : 实际登记佣金明细;不传时只预览}';
 
     protected $description = '从已有充值返现流水补记分享佣金明细,不修改钱包或再次发奖';
@@ -20,22 +24,34 @@ class BackfillShareCommissions extends Command
     public function handle(ShareCommissionRecorder $recorder): int
     {
         try {
-            $end = $this->parseDate((string)($this->option('end-date') ?: date('Y-m-d')));
-            $start = $this->parseDate((string)($this->option('start-date') ?: $end->copy()->subDays(30)->toDateString()));
-            if ($start->gt($end) || $start->diffInDays($end) > 366) {
-                throw new \InvalidArgumentException('日期范围须先后有效,单次不超过366天');
+            $all = (bool)$this->option('all');
+            $diagnose = (bool)$this->option('diagnose');
+            $write = (bool)$this->option('write');
+            if ($diagnose && $write) throw new \InvalidArgumentException('--diagnose 为只读诊断,不能与 --write 同时使用');
+            if ($all && ($this->option('start-date') !== null || $this->option('end-date') !== null)) {
+                throw new \InvalidArgumentException('--all 不能与日期选项同时使用');
+            }
+            $range = null;
+            if (!$all) {
+                $end = $this->parseDate((string)($this->option('end-date') ?: date('Y-m-d')));
+                $start = $this->parseDate((string)($this->option('start-date') ?: $end->copy()->subDays(30)->toDateString()));
+                if ($start->gt($end) || $start->diffInDays($end) > 366) {
+                    throw new \InvalidArgumentException('日期范围须先后有效,单次不超过366天');
+                }
+                $range = [$start->toDateString() . ' 00:00:00', $end->toDateString() . ' 23:59:59'];
             }
             $chunk = filter_var($this->option('chunk'), FILTER_VALIDATE_INT);
             if ($chunk === false || $chunk < 1 || $chunk > 2000) {
                 throw new \InvalidArgumentException('chunk 必须是1至2000的整数');
             }
-            $write = (bool)$this->option('write');
+            $label = $all ? '全部历史(无日期过滤)' : $start->toDateString() . ' 至 ' . $end->toDateString();
+            if ($diagnose) return $this->diagnose($range, $label);
             $counts = ['scanned' => 0, 'created' => 0, 'pending' => 0, 'existing' => 0, 'skipped' => 0, 'incomplete_metadata' => 0];
             $reasons = [];
-            $this->info(($write ? '写入登记' : '只读预览') . ":{$start->toDateString()} 至 {$end->toDateString()}");
-            BalanceLog::where('change_type', '充值返现')->where('type', 1)->where('amount', '>', 0)
-                ->whereBetween('created_at', [$start->toDateString() . ' 00:00:00', $end->toDateString() . ' 23:59:59'])
-                ->chunkById($chunk, function ($rows) use ($recorder, $write, &$counts, &$reasons) {
+            $this->info(($write ? '写入登记' : '只读预览') . ':' . $label);
+            $query = BalanceLog::where('change_type', '充值返现')->where('type', 1)->where('amount', '>', 0);
+            if ($range !== null) $query->whereBetween('created_at', $range);
+            $query->chunkById($chunk, function ($rows) use ($recorder, $write, &$counts, &$reasons) {
                     foreach ($rows as $row) {
                         $result = $write ? $recorder->record($row) : $recorder->preview($row);
                         $counts['scanned']++;
@@ -49,6 +65,9 @@ class BackfillShareCommissions extends Command
                 });
             $this->table(['项目', '条数'], array_map(fn ($key, $count) => [$key, $count], array_keys($counts), array_values($counts)));
             foreach ($reasons as $reason => $count) $this->line("跳过 {$reason}: {$count}");
+            if (!$counts['scanned']) {
+                $this->warn('没有扫描到符合条件的返现流水;省略日期仍默认最近31天。可运行 --diagnose --all 定位上游数据。');
+            }
             if (!$write) $this->info('预览未写入数据;确认后使用相同日期并加 --write 登记,钱包不会再次入账。');
             return self::SUCCESS;
         } catch (\Throwable $e) {
@@ -57,6 +76,36 @@ class BackfillShareCommissions extends Command
         }
     }
 
+    private function diagnose(?array $range, string $label): int
+    {
+        $this->info('只读诊断:' . $label);
+        $this->line('database: ' . DB::connection()->getDatabaseName());
+        $this->line('table_prefix: ' . DB::getTablePrefix());
+        $data = (new ShareCommissionDiagnostics())->inspect($range);
+        foreach (['recharge_rate', 'share_enabled', 'tier_one_rate', 'tier_two_rate', 'first_rebate_at', 'last_rebate_at'] as $key) {
+            $this->line($key . ': ' . ($data[$key] ?? '(无)'));
+        }
+        $this->table(['检查项', '条数'], array_map(fn ($key, $count) => [$key, $count], array_keys($data['counts']), array_values($data['counts'])));
+        $counts = $data['counts'];
+        if (!$counts['rebate_logs_all']) $this->warn('全部历史没有“充值返现”流水,回填没有来源;需检查实际返现是否发生。');
+        elseif (!$counts['positive_type1_rebate_logs_all']) $this->warn('存在返现流水,但没有 type=1 且金额大于0的可登记流水。');
+        elseif (!$counts['selected_rebate_logs']) $this->warn('全历史有可登记返现,但当前日期范围没有;按原入账日期查询或使用 --all。');
+        if ($data['recharge_rate'] === null || !is_numeric($data['recharge_rate']) || (float)$data['recharge_rate'] <= 0) {
+            $this->warn('当前 recharge_rate 缺失、无效或为0,现有邀请充值返现路径不会产生正数返现。');
+        }
+        if ($counts['invited_members_all'] && !$counts['real_invited_members_all']) $this->warn('有推荐注册,但没有 from=1 的真实被邀请会员。');
+        if ($counts['real_invited_members_all'] && !$counts['linked_real_invited_members_all']) $this->warn('真实被邀请会员的邀请码未匹配到有效推广人。');
+        if ($counts['real_invited_members_all'] && !$counts['real_invitee_deposit_logs_all']) $this->warn('真实被邀请会员没有正数“充值/三方充值”流水;人工充值不触发现有邀请返现。');
+        if ($counts['linked_real_invitee_deposit_logs_all'] && !$counts['with_promoter_wallet_deposit_logs_all']) $this->warn('有推荐充值,但推广人钱包缺失。');
+        if (!$counts['positive_type1_rebate_logs_all'] && $counts['with_promoter_wallet_deposit_logs_all']
+            && is_numeric($data['recharge_rate']) && (float)$data['recharge_rate'] > 0) {
+            $this->warn('当前关系下有充值和推广人钱包,但没有正数返现;需核对当时配置、充值入口是否执行邀请返现,以及单笔返现是否不足0.01。');
+        }
+        $this->line('实际旧返现使用 config.recharge_rate;分享设定中的一级/二级比例是另一套配置。');
+        $this->line('上游数量按当前关系统计,仅供排查,不代表历史应付佣金;本诊断未写入记录或发钱。');
+        return self::SUCCESS;
+    }
+
     private function parseDate(string $value): Carbon
     {
         $date = Carbon::createFromFormat('!Y-m-d', $value, config('app.timezone', 'Asia/Shanghai'));

+ 88 - 0
app/Services/ShareCommissionDiagnostics.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\BalanceLog;
+use App\Models\Config;
+use App\Models\ShareCommissionRecord;
+use App\Models\User;
+use Illuminate\Support\Facades\DB;
+
+/** Read-only counts that locate the missing stage; never calculates or grants unpaid rewards. */
+class ShareCommissionDiagnostics
+{
+    public function inspect(?array $range): array
+    {
+        $rebates = BalanceLog::where('change_type', '充值返现')->selectRaw('COUNT(*) AS total')
+            ->selectRaw('COALESCE(SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END), 0) AS type1')
+            ->selectRaw('COALESCE(SUM(CASE WHEN type = 1 AND amount > 0 THEN 1 ELSE 0 END), 0) AS positive')
+            ->selectRaw('MIN(CASE WHEN type = 1 AND amount > 0 THEN created_at END) AS first_rebate_at')
+            ->selectRaw('MAX(CASE WHEN type = 1 AND amount > 0 THEN created_at END) AS last_rebate_at')->first();
+        $selected = BalanceLog::where('change_type', '充值返现')->where('type', 1)->where('amount', '>', 0);
+        if ($range !== null) $selected->whereBetween('created_at', $range);
+
+        $invited = User::query()->from('users as invitee')
+            ->whereNotNull('invitee.agent_user_code')->where('invitee.agent_user_code', '<>', '');
+        $realInvited = (clone $invited)->where('invitee.from', 1);
+        $linked = (clone $realInvited)->where('invitee.agent_user_code', '<>', '0')->whereExists(function ($q) {
+            $q->selectRaw('1')->from('users as promoter')
+                ->whereColumn('promoter.user_code', 'invitee.agent_user_code')
+                ->whereNotNull('promoter.member_id')->where('promoter.member_id', '<>', '')->where('promoter.member_id', '<>', '0');
+        });
+
+        $deposits = BalanceLog::where('type', 1)->where('amount', '>', 0)
+            ->whereIn('change_type', ['充值', '三方充值']);
+        $realDeposits = (clone $deposits)->whereExists(function ($q) {
+            $q->selectRaw('1')->from('users as invitee')
+                ->whereColumn('invitee.member_id', 'balance_logs.member_id')->where('invitee.from', 1)
+                ->whereNotNull('invitee.agent_user_code')->where('invitee.agent_user_code', '<>', '');
+        });
+        $linkedDeposits = (clone $realDeposits)->whereExists(function ($q) {
+            $q->selectRaw('1')->from('users as invitee')
+                ->join('users as promoter', 'promoter.user_code', '=', 'invitee.agent_user_code')
+                ->whereColumn('invitee.member_id', 'balance_logs.member_id')
+                ->where('invitee.from', 1)->whereNotNull('invitee.agent_user_code')->where('invitee.agent_user_code', '<>', '')->where('invitee.agent_user_code', '<>', '0')
+                ->whereNotNull('promoter.member_id')->where('promoter.member_id', '<>', '')->where('promoter.member_id', '<>', '0');
+        });
+        $walletDeposits = (clone $linkedDeposits)->whereExists(function ($q) {
+            $q->selectRaw('1')->from('users as invitee')
+                ->join('users as promoter', 'promoter.user_code', '=', 'invitee.agent_user_code')
+                ->join('wallets as promoter_wallet', 'promoter_wallet.member_id', '=', 'promoter.member_id')
+                ->whereColumn('invitee.member_id', 'balance_logs.member_id')
+                ->where('invitee.from', 1)->whereNotNull('invitee.agent_user_code')->where('invitee.agent_user_code', '<>', '')->where('invitee.agent_user_code', '<>', '0')
+                ->whereNotNull('promoter.member_id')->where('promoter.member_id', '<>', '')->where('promoter.member_id', '<>', '0');
+        });
+        $activeRecords = ShareCommissionRecord::where('status', 1);
+        if ($range !== null) {
+            $activeRecords->whereBetween('stat_date', [substr($range[0], 0, 10), substr($range[1], 0, 10)]);
+        }
+        $rate = Config::where('field', 'recharge_rate')->value('val');
+        $setting = DB::connection()->getSchemaBuilder()->hasTable('share_settings')
+            ? DB::table('share_settings')->orderBy('id')->first(['enabled', 'tier_one_rate', 'tier_two_rate']) : null;
+        return [
+            'database' => DB::connection()->getDatabaseName(),
+            'table_prefix' => DB::getTablePrefix(),
+            'recharge_rate' => $rate,
+            'share_enabled' => $setting->enabled ?? null,
+            'tier_one_rate' => $setting->tier_one_rate ?? null,
+            'tier_two_rate' => $setting->tier_two_rate ?? null,
+            'counts' => [
+                'rebate_logs_all' => (int)$rebates->total,
+                'type1_rebate_logs_all' => (int)$rebates->type1,
+                'positive_type1_rebate_logs_all' => (int)$rebates->positive,
+                'selected_rebate_logs' => $selected->count(),
+                'invited_members_all' => $invited->count(),
+                'real_invited_members_all' => $realInvited->count(),
+                'linked_real_invited_members_all' => $linked->count(),
+                'deposit_logs_all' => $deposits->count(),
+                'real_invitee_deposit_logs_all' => $realDeposits->count(),
+                'linked_real_invitee_deposit_logs_all' => $linkedDeposits->count(),
+                'with_promoter_wallet_deposit_logs_all' => $walletDeposits->count(),
+                'commission_records_all' => ShareCommissionRecord::count(),
+                'active_commission_records_selected' => $activeRecords->count(),
+            ],
+            'first_rebate_at' => $rebates->getRawOriginal('first_rebate_at'),
+            'last_rebate_at' => $rebates->getRawOriginal('last_rebate_at'),
+        ];
+    }
+}

+ 29 - 2
docs/后端/分享赚钱-推广汇总明细登记与历史回填.md

@@ -20,6 +20,14 @@
 
 ## 历史回填
 
+**省略日期仍然只查最近 31 天,不表示全历史。**若运行后所有计数为 `0`,先在发布本次命令更新后执行:
+
+```bash
+php artisan share:backfill-commissions --diagnose --all
+```
+
+这是只读诊断,不登记、不发钱,不能和 `--write` 同用。它会输出当前连接的数据库名、表前缀、旧返现比例、分享设定、全历史返现筛选及推荐注册/充值数据,便于比较 CLI 与网站是否连接同一业务数据库。
+
 先发布新代码,在连接正确业务数据库的后端目录执行预览。以下范围对应 2026-10-23 的默认最近 31 天:
 
 ```bash
@@ -39,9 +47,28 @@ php artisan share:backfill-commissions --start-date=2026-09-23 --end-date=2026-1
 | `--start-date` | 返现流水入账起始日,格式 `Y-m-d`;默认结束日前 30 天 |
 | `--end-date` | 入账结束日;默认应用服务器当天 |
 | `--chunk` | 每批读取条数,默认 `500`,范围 `1`~`2000` |
+| `--all` | 明确查询全部历史,不能与起止日期选项同用;可用于预览或实际登记 |
+| `--diagnose` | 只读输出数据链路统计,不执行登记;可与 `--all` 同用,与 `--write` 互斥 |
 | `--write` | 实际登记;省略时仅预览 |
 
-单次日期范围不超过 366 天。历史更长时按月份或年度分批执行。默认范围与汇总接口一致,但统计的是返现流水日期,不是会员注册日期。
+指定日期时单次范围不超过 366 天;历史更长可按月份或年度分批执行。默认范围与汇总接口一致,但统计的是返现流水日期,不是会员注册日期。全历史预览使用 `--all`,实际登记再加 `--write`。历史明细保留原入账日期;回填旧日期后,汇总 API 仍需传对应日期才能查看,不会统一改记为今天。
+
+### 诊断结果对应断点
+
+| 检查项 / 结果 | 排查方向 |
+|---|---|
+| `rebate_logs_all=0` | 全历史没有「充值返现」流水,历史回填没有来源;推荐注册有数据不等于已经产生返现 |
+| 有返现但 `type1_rebate_logs_all=0` | 被 `type=1` 条件排除,核对流水类别口径 |
+| 有 type=1 返现但 `positive_type1_rebate_logs_all=0` | 金额均不大于0,不能登记为已付正佣金 |
+| 全历史正返现有数据,`selected_rebate_logs=0` | 所选日期未覆盖原返现入账时间,参考 `first_rebate_at/last_rebate_at` |
+| `recharge_rate` 为0、缺失或无效 | 当前旧邀请返现入口不能产生正数返现;该值来自 `config`,不是分享页面的一级/二级比例 |
+| `invited_members_all>0`,`real_invited_members_all=0` | 推荐注册会员都不是现有返现路径要求的 `from=1` 真实用户 |
+| `linked_real_invited_members_all=0` | 邀请码无法匹配到推广人 `user_code` 及有效业务会员 ID |
+| `real_invitee_deposit_logs_all=0` | 真实被邀请会员没有正数「充值/三方充值」流水;人工充值不触发现有邀请返现 |
+| 有匹配推荐的充值,但 `with_promoter_wallet_deposit_logs_all=0` | 推广人钱包缺失,不能完成原返现入账 |
+| 关联、充值、钱包及当前比例都有,但没有已付返现 | 核对充值发生时的比例/关系、真实入账入口是否调用邀请返现,以及两位小数截断后的单笔返现是否为0 |
+
+带 `_all` 的上游数量是全历史、按当前会员关系统计;`selected_rebate_logs` 和 `active_commission_records_selected` 使用所选日期范围。诊断结果仅定位数据来源,不据此认定历史应付金额或自动补发佣金。此处不会自动修改 `recharge_rate` 或把分享设置比例替代旧资金规则。
 
 ## 输出与缺失数据
 
@@ -70,7 +97,7 @@ php artisan share:backfill-commissions --start-date=2026-09-23 --end-date=2026-1
 
 成功码为 `0`,查看 `data.total`、`data.data` 及 `data.summary.total_commission`。如果命令 `scanned=0`,说明范围内没有已付返现可补;推荐会员有数据不等于已经产生佣金。
 
-`tests/Integration/ShareCommissionRecordingTest.php` 使用真实控制器、资金服务、登记服务和命令,连接隔离的 SQLite 内存库,覆盖新返现、接口汇总、原发放条件、故障回滚、历史日期、比例变更、缺失元数据、只读预览和重复回填;不会连接生产库或执行真实充值。
+`tests/Integration/ShareCommissionRecordingTest.php` 使用真实控制器、资金服务、登记服务和命令,连接隔离的 SQLite 内存库,覆盖新返现、接口汇总、原发放条件、故障回滚、历史日期、比例变更、缺失元数据、只读预览、重复回填及全历史诊断;不会连接生产库或执行真实充值。
 
 ```bash
 php -d error_reporting=24575 vendor/bin/phpunit --bootstrap tests/bootstrap-agent.php tests/Integration/ShareCommissionRecordingTest.php --do-not-cache-result

+ 117 - 0
tests/Integration/ShareCommissionRecordingTest.php

@@ -8,6 +8,7 @@ use App\Models\BalanceLog;
 use App\Models\ShareCommissionRecord;
 use App\Services\BalanceLogService;
 use App\Services\ShareCommissionRecorder;
+use App\Services\ShareCommissionDiagnostics;
 use App\Services\ShareEarningReportService;
 use Illuminate\Config\Repository;
 use Illuminate\Database\Capsule\Manager;
@@ -332,4 +333,120 @@ class ShareCommissionRecordingTest extends TestCase
         $this->assertSame(1, $result->getStatusCode());
         $this->assertSame(0, ShareCommissionRecord::count());
     }
+
+    public function test_omitting_dates_is_not_full_history_and_all_can_backfill_old_receipts(): void
+    {
+        $rebate = $this->historicalRebate(['created_at' => '2020-01-01 00:00:00', 'updated_at' => '2020-01-01 00:00:00']);
+        $default = $this->command([]);
+        $this->assertSame(0, $default->getStatusCode());
+        $this->assertStringContainsString('省略日期仍默认最近31天', $default->getDisplay());
+        $this->assertSame(0, ShareCommissionRecord::count());
+        $all = $this->command(['--all' => true]);
+        $this->assertSame(0, $all->getStatusCode());
+        $this->assertStringContainsString('全部历史', $all->getDisplay());
+        $this->assertSame(0, ShareCommissionRecord::count());
+        $written = $this->command(['--all' => true, '--write' => true]);
+        $this->assertSame(0, $written->getStatusCode());
+        $this->assertSame((string)$rebate->id, ShareCommissionRecord::firstOrFail()->source_id);
+        $this->assertSame('2020-01-01', ShareCommissionRecord::firstOrFail()->stat_date->format('Y-m-d'));
+        $this->assertEquals('22.50', DB::table('wallets')->where('member_id', '1001')->value('available_balance'));
+    }
+
+    private function depositForDiagnosis(): void
+    {
+        BalanceLog::create(['member_id' => '1002', 'type' => 1, 'change_type' => '充值',
+            'amount' => '100.00', 'before_balance' => '0.00', 'after_balance' => '100.00', 'remark' => '']);
+    }
+
+    public function test_diagnostics_show_a_zero_legacy_rate_despite_configured_share_settings_without_writing(): void
+    {
+        $this->depositForDiagnosis();
+        DB::table('config')->where('field', 'recharge_rate')->update(['val' => '0']);
+        DB::connection()->getSchemaBuilder()->create('share_settings', function (Blueprint $t) {
+            $t->id(); $t->integer('enabled'); $t->decimal('tier_one_rate', 8, 4); $t->decimal('tier_two_rate', 8, 4);
+        });
+        DB::table('share_settings')->insert(['enabled' => 1, 'tier_one_rate' => '4.00', 'tier_two_rate' => '6.00']);
+        $tables = ['users', 'wallets', 'balance_logs', 'share_commission_records', 'config', 'share_settings'];
+        $before = [];
+        foreach ($tables as $table) $before[$table] = DB::table($table)->orderBy('id')->get()->toJson();
+        $data = (new ShareCommissionDiagnostics())->inspect(null);
+        $this->assertSame(':memory:', $data['database']);
+        $this->assertSame('bot_', $data['table_prefix']);
+        $this->assertSame('0', $data['recharge_rate']);
+        $this->assertEquals(4, $data['tier_one_rate']);
+        $this->assertSame(0, $data['counts']['rebate_logs_all']);
+        $this->assertSame(1, $data['counts']['real_invitee_deposit_logs_all']);
+        $this->assertSame(1, $data['counts']['linked_real_invitee_deposit_logs_all']);
+        $this->assertSame(1, $data['counts']['with_promoter_wallet_deposit_logs_all']);
+        $run = $this->command(['--diagnose' => true, '--all' => true]);
+        $this->assertSame(0, $run->getStatusCode(), $run->getDisplay());
+        $this->assertStringContainsString('全部历史没有', $run->getDisplay());
+        $this->assertStringContainsString('当前 recharge_rate 缺失、无效或为0', $run->getDisplay());
+        $this->assertStringContainsString('share_enabled: 1', $run->getDisplay());
+        foreach ($tables as $table) $this->assertSame($before[$table], DB::table($table)->orderBy('id')->get()->toJson(), $table);
+    }
+
+    public function test_diagnostics_separate_type_amount_and_date_filters(): void
+    {
+        $this->historicalRebate(['created_at' => '2020-01-01 00:00:00']);
+        $this->historicalRebate(['type' => 2]);
+        $this->historicalRebate(['amount' => '0', 'after_balance' => '20']);
+        $selected = $this->historicalRebate();
+        (new ShareCommissionRecorder())->record($selected);
+        $data = (new ShareCommissionDiagnostics())->inspect(['2026-10-01 00:00:00', '2026-10-02 23:59:59']);
+        $this->assertSame(4, $data['counts']['rebate_logs_all']);
+        $this->assertSame(3, $data['counts']['type1_rebate_logs_all']);
+        $this->assertSame(2, $data['counts']['positive_type1_rebate_logs_all']);
+        $this->assertSame(1, $data['counts']['selected_rebate_logs']);
+        $this->assertSame('2020-01-01 00:00:00', $data['first_rebate_at']);
+        $this->assertSame('2026-10-01 01:02:03', $data['last_rebate_at']);
+        $this->assertSame(1, $data['counts']['active_commission_records_selected']);
+    }
+
+    public static function brokenReferralStages(): array
+    {
+        return [
+            [2, 'PROMO', false, [0, 0, 0, 0, 0]],
+            [1, 'MISSING', false, [1, 0, 1, 0, 0]],
+            [1, 'PROMO', true, [1, 1, 1, 1, 0]],
+        ];
+    }
+
+    /** @dataProvider brokenReferralStages */
+    public function test_diagnostics_locate_real_user_referral_and_wallet_gaps(int $from, string $code, bool $missingWallet, array $expected): void
+    {
+        DB::table('users')->where('member_id', '1002')->update(['from' => $from, 'agent_user_code' => $code]);
+        if ($missingWallet) DB::table('wallets')->where('member_id', '1001')->delete();
+        $this->depositForDiagnosis();
+        $counts = (new ShareCommissionDiagnostics())->inspect(null)['counts'];
+        $this->assertSame(1, $counts['invited_members_all']);
+        foreach (['real_invited_members_all', 'linked_real_invited_members_all', 'real_invitee_deposit_logs_all',
+            'linked_real_invitee_deposit_logs_all', 'with_promoter_wallet_deposit_logs_all'] as $i => $key) {
+            $this->assertSame($expected[$i], $counts[$key], $key);
+        }
+    }
+
+    public function test_read_only_diagnostics_cannot_be_combined_with_write_or_all_with_dates(): void
+    {
+        $this->historicalRebate();
+        $run = $this->command(['--diagnose' => true, '--write' => true]);
+        $this->assertSame(1, $run->getStatusCode());
+        $this->assertSame(0, ShareCommissionRecord::count());
+        $conflict = $this->command(['--all' => true, '--start-date' => '2026-10-01', '--write' => true]);
+        $this->assertSame(1, $conflict->getStatusCode());
+        $this->assertSame(0, ShareCommissionRecord::count());
+    }
+
+    public function test_diagnostic_referral_matches_the_existing_php_empty_zero_code_rule(): void
+    {
+        DB::table('users')->where('member_id', '1001')->update(['user_code' => '0']);
+        DB::table('users')->where('member_id', '1002')->update(['agent_user_code' => '0']);
+        $this->depositForDiagnosis();
+        $counts = (new ShareCommissionDiagnostics())->inspect(null)['counts'];
+        $this->assertSame(1, $counts['invited_members_all']);
+        $this->assertSame(1, $counts['real_invited_members_all']);
+        $this->assertSame(0, $counts['linked_real_invited_members_all']);
+        $this->assertSame(0, $counts['linked_real_invitee_deposit_logs_all']);
+        $this->assertSame(0, $counts['with_promoter_wallet_deposit_logs_all']);
+    }
 }