فهرست منبع

会员查询,日期所有

doge 2 روز پیش
والد
کامیت
634d122f2c

+ 6 - 4
app/Services/ShareEarningReportService.php

@@ -13,12 +13,14 @@ class ShareEarningReportService
 {
     public function memberDays(array $params): array
     {
-        [$start, $end] = $this->dateRange($params);
+        $range = !empty($params['start_date']) || !empty($params['end_date']) ? $this->dateRange($params) : null;
         $page = (int)($params['page'] ?? 1);
         $limit = (int)($params['limit'] ?? 20);
-        $dateQuery = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '')
-            ->whereBetween('created_at', [$start . ' 00:00:00', $end . ' 23:59:59'])
-            ->selectRaw('DATE(created_at) AS stat_date')
+        $dateQuery = User::query()->whereNotNull('agent_user_code')->where('agent_user_code', '<>', '');
+        if ($range !== null) {
+            $dateQuery->whereBetween('created_at', [$range[0] . ' 00:00:00', $range[1] . ' 23:59:59']);
+        }
+        $dateQuery->selectRaw('DATE(created_at) AS stat_date')
             ->groupByRaw('DATE(created_at)')
             ->orderByDesc('stat_date');
         $dates = $dateQuery->pluck('stat_date');

+ 15 - 1
docs/前端接口/分享赚钱/推广会员列表/接口.md

@@ -4,9 +4,23 @@
 
 ## 按注册日汇总
 
+`GET /admin/shareEarning/members?page=1&limit=20`
+
+起止日期均不传、为 `null` 或为空字符串时,查询全部历史注册日期,仍按页返回。选择日期时,发送完整起止日期,例如:
+
 `GET /admin/shareEarning/members?start_date=2026-08-01&end_date=2026-08-30&page=1&limit=20`
 
-日期不传时默认最近 31 天。
+| 参数 | 说明 |
+|---|---|
+| `start_date/end_date` | 可选,格式 `Y-m-d`,按被邀请会员的注册日期筛选,包含起止当天;显式范围不能超过 366 天 |
+| `page` | 页码,默认 `1`,最小 `1` |
+| `limit` | 每页注册日期数,默认 `20`,范围 `1`~`200` |
+
+清空日期选择器时,应同时移除两个日期参数或将两者置空,避免继续提交上次选中的日期。
+
+返回 `data.total` 是有推荐注册会员的**日期数**,`data.data` 每条是一天的汇总,不是单个会员;同一天有 10 名会员注册也只返回一条,人数见 `registered_members`。按注册日期倒序分页,不补没有推荐注册的日期。
+
+数据直接来自会员表已有推荐关系,不依赖 `share_commission_records`,无需执行佣金回填脚本。日期筛选只限制会员的注册日期,下表的充值、投注等金额仍统计这些会员的累计流水。
 
 | 返回字段 | 中文含义 |
 |---|---|

+ 1 - 1
docs/前端接口/分享赚钱/推广汇总/接口.md

@@ -22,7 +22,7 @@
 /admin/shareEarning/summary?start_date=2026-09-23&end_date=2026-10-23&page=1&limit=10
 ```
 
-前端清空时间选择器时,省略两个日期参数,或都传空字符串;不要继续携带上次选中的日期。显式日期范围仍最多支持 366 天,无日期筛选时可跨年查询。该默认行为仅适用于 `summary`;推广会员列表及回填命令仍沿用各自原有的默认范围。
+前端清空时间选择器时,省略两个日期参数,或都传空字符串;不要继续携带上次选中的日期。显式日期范围仍最多支持 366 天,无日期筛选时可跨年查询。`summary` 和推广会员列表 `members` 均默认全部日期;回填命令仍默认最近 31 天。
 
 响应:
 

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

@@ -2,6 +2,8 @@
 
 `/admin/shareEarning/members` 读取推荐注册关系,`/admin/shareEarning/summary` 读取已生成的 `share_commission_records`。会员列表有数据、佣金明细表为空时,汇总列表会为空。
 
+这两个 API 在起止日期均未传或为空时都查询全部历史日期,仍按页返回;显式日期只查询所选范围。`members` 每条是一个注册日期的汇总,`total` 是日期数,人数看 `registered_members`。它直接读取 `users.agent_user_code` 推荐关系和 `users.created_at` 注册日期,无需历史回填;没有推荐关系的会员不纳入此报表。
+
 本次接通已有邀请返现的明细登记,保留现有 `config.recharge_rate`、真实用户充值及原推荐关系的发放条件,不新增一级/二级、投注返佣、手续费减免或其他资金规则。
 
 ## 新返现
@@ -97,7 +99,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`,说明范围内没有已付返现可补;推荐会员有数据不等于已经产生佣金。
 
-检查全部日期的已登记明细时,请使用 `/admin/shareEarning/summary?account=&page=1&limit=10`,省略起止日期;返回仍按页限制,合计覆盖全部符合条件的记录。仅该汇总 API 改为默认全部日期,推广会员列表和回填命令的默认最近 31 天保持不变。
+检查全部日期的已登记明细时,请使用 `/admin/shareEarning/summary?account=&page=1&limit=10`,省略起止日期;返回仍按页限制,合计覆盖全部符合条件的记录。全部历史推荐注册按日汇总使用 `/admin/shareEarning/members?page=1&limit=10`。两者均默认全部日期;回填命令仍默认最近 31 天,需要全历史回填时单独使用 `--all`。
 
 `tests/Integration/ShareCommissionRecordingTest.php` 使用真实控制器、资金服务、登记服务和命令,连接隔离的 SQLite 内存库,覆盖新返现、接口汇总、原发放条件、故障回滚、历史日期、比例变更、缺失元数据、只读预览、重复回填及全历史诊断;不会连接生产库或执行真实充值。
 

+ 117 - 7
tests/Integration/ShareCommissionRecordingTest.php

@@ -479,7 +479,7 @@ class ShareCommissionRecordingTest extends TestCase
         })->getData(true);
     }
 
-    public static function clearedSummaryDates(): array
+    public static function clearedReportDates(): array
     {
         return [
             'omitted' => [[]],
@@ -488,7 +488,7 @@ class ShareCommissionRecordingTest extends TestCase
         ];
     }
 
-    /** @dataProvider clearedSummaryDates */
+    /** @dataProvider clearedReportDates */
     public function test_summary_without_dates_includes_all_history_and_keeps_pagination_and_global_totals(array $dates): void
     {
         $this->summaryHistory();
@@ -538,15 +538,125 @@ class ShareCommissionRecordingTest extends TestCase
         $this->assertSame(['promoter', 'promoter'], array_column($result['data']['data'], 'promoter_account'));
     }
 
-    public function test_summary_explicit_range_limits_and_other_report_defaults_remain_unchanged(): void
+    public function test_summary_explicit_range_limit_remains_enforced(): void
     {
         $this->summaryHistory();
         $invalid = $this->summaryResponse(['start_date' => '2020-01-01', 'end_date' => '2026-10-01']);
         $this->assertSame(-3, $invalid['code']);
         $this->assertStringContainsString('366', $invalid['msg']);
-        DB::table('users')->update(['created_at' => '2020-01-01 00:00:00']);
-        $other = (new ShareEarningReportService())->memberDays([]);
-        $this->assertSame(0, $other['total']);
-        $this->assertSame([], $other['data']);
+    }
+
+    private function memberHistory(): void
+    {
+        DB::connection()->getSchemaBuilder()->create('share_settings', function (Blueprint $t) {
+            $t->id(); $t->decimal('valid_member_min_deposit', 20, 2);
+        });
+        DB::table('share_settings')->insert(['valid_member_min_deposit' => 20]);
+        DB::connection()->getSchemaBuilder()->create('user_login', function (Blueprint $t) {
+            $t->id(); $t->string('user_id'); $t->integer('status'); $t->string('platform');
+        });
+        DB::table('users')->where('id', 42)->update(['created_at' => '2020-01-01 00:00:00']);
+        foreach ([
+            [43, '1003', 'OTHER', '2020-01-01 23:59:59'],
+            [44, '1004', 'PROMO', '2021-06-15 12:00:00'],
+            [45, '1005', 'PROMO', date('Y-m-d') . ' 12:00:00'],
+            [46, '1006', null, '2019-01-01 12:00:00'],
+            [47, '1007', '', '2018-01-01 12:00:00'],
+        ] as [$id, $memberId, $code, $createdAt]) {
+            DB::table('users')->insert([
+                'id' => $id, 'member_id' => $memberId, 'account' => 'member' . $memberId,
+                'first_name' => '会员', 'user_code' => 'CODE' . $memberId,
+                'agent_user_code' => $code, 'created_at' => $createdAt,
+            ]);
+        }
+        foreach ([
+            ['1002', '充值', 100], ['1002', '提现', -10],
+            ['1002', '投注', -20], ['1002', '优惠活动', 5], ['1003', '三方充值', 30],
+        ] as [$memberId, $type, $amount]) {
+            DB::table('balance_logs')->insert([
+                'member_id' => $memberId, 'change_type' => $type, 'amount' => $amount,
+                'before_balance' => 100, 'after_balance' => 100 + $amount,
+                'created_at' => date('Y-m-d H:i:s'),
+            ]);
+        }
+        DB::table('user_login')->insert([
+            ['user_id' => '1002', 'status' => 0, 'platform' => 'Android'],
+            ['user_id' => '1002', 'status' => 1, 'platform' => 'H5'],
+            ['user_id' => '1002', 'status' => 1, 'platform' => 'Android'],
+            ['user_id' => '1003', 'status' => 1, 'platform' => 'Android'],
+            ['user_id' => '1004', 'status' => 1, 'platform' => 'iOS'],
+        ]);
+    }
+
+    private function membersResponse(array $params): array
+    {
+        $request = Request::create('/admin/shareEarning/members', 'GET', $params);
+        return (new \App\Http\Middleware\TrimStrings())->handle($request, function ($request) {
+            return (new \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull())->handle($request, function ($request) {
+                $this->app->instance('request', $request);
+                return (new ShareEarning())->memberList(new ShareEarningReportService());
+            });
+        })->getData(true);
+    }
+
+    /** @dataProvider clearedReportDates */
+    public function test_members_without_dates_includes_historical_registration_days_without_backfill(array $dates): void
+    {
+        $this->memberHistory();
+        $users = DB::table('users')->orderBy('id')->get()->toJson();
+        $logs = DB::table('balance_logs')->orderBy('id')->get()->toJson();
+        $result = $this->membersResponse($dates + ['page' => 1, 'limit' => 1]);
+        $this->assertSame(0, $result['code']);
+        $this->assertSame(3, $result['data']['total']);
+        $this->assertCount(1, $result['data']['data']);
+        $this->assertSame(date('Y-m-d'), $result['data']['data'][0]['date']);
+        $last = $this->membersResponse($dates + ['page' => 3, 'limit' => 1]);
+        $this->assertSame(3, $last['data']['total']);
+        $row = $last['data']['data'][0];
+        $this->assertSame('2020-01-01', $row['date']);
+        $this->assertSame(2, $row['registered_members']);
+        $this->assertSame(2, $row['promoter_count']);
+        $this->assertSame(1, $row['h5_registered']);
+        $this->assertSame(1, $row['android_registered']);
+        $this->assertSame(0, $row['ios_registered']);
+        $this->assertSame('130.0000', $row['deposit_total']);
+        $this->assertSame('20.0000', $row['consume_total']);
+        $this->assertSame('115.0000', $row['company_profit']);
+        $this->assertSame($users, DB::table('users')->orderBy('id')->get()->toJson());
+        $this->assertSame($logs, DB::table('balance_logs')->orderBy('id')->get()->toJson());
+        $this->assertSame(0, ShareCommissionRecord::count());
+    }
+
+    public function test_members_explicit_dates_filter_registration_days_but_keep_lifetime_money_metrics(): void
+    {
+        $this->memberHistory();
+        $result = $this->membersResponse(['start_date' => '2020-01-01', 'end_date' => '2020-01-01']);
+        $this->assertSame(0, $result['code']);
+        $this->assertSame(1, $result['data']['total']);
+        $this->assertCount(1, $result['data']['data']);
+        $row = $result['data']['data'][0];
+        $this->assertSame('2020-01-01', $row['date']);
+        $this->assertSame(2, $row['registered_members']);
+        $this->assertSame('130.0000', $row['deposit_total']);
+        $other = $this->membersResponse(['start_date' => '2021-06-15', 'end_date' => '2021-06-15']);
+        $this->assertSame(0, $other['code']);
+        $this->assertSame(1, $other['data']['total']);
+        $this->assertSame(1, $other['data']['data'][0]['ios_registered']);
+        $this->assertSame('0.0000', $other['data']['data'][0]['deposit_total']);
+    }
+
+    public function test_members_empty_days_and_explicit_range_limit(): void
+    {
+        $this->memberHistory();
+        $empty = $this->membersResponse(['start_date' => '2019-01-01', 'end_date' => '2019-01-01']);
+        $this->assertSame(0, $empty['code']);
+        $this->assertSame(0, $empty['data']['total']);
+        $this->assertSame([], $empty['data']['data']);
+        $pastLastPage = $this->membersResponse(['page' => 4, 'limit' => 1]);
+        $this->assertSame(3, $pastLastPage['data']['total']);
+        $this->assertSame([], $pastLastPage['data']['data']);
+        $invalid = $this->membersResponse(['start_date' => '2020-01-01', 'end_date' => '2026-10-01']);
+        $this->assertSame(-3, $invalid['code']);
+        $this->assertStringContainsString('366', $invalid['msg']);
     }
 }