瀏覽代碼

代理按域名,不加path

doge 4 天之前
父節點
當前提交
aa7602bdf5

+ 15 - 15
app/Services/Agent/AgentDomainService.php

@@ -6,6 +6,10 @@ use App\Models\Agent\Agent;
 use App\Models\Agent\AgentDomain;
 use Illuminate\Support\Facades\DB;
 
+/**
+ * 代理推广域名:总后台绑定什么就保存什么(含路径和参数,原样返回作为推广链接),不再自动拼注册路径和邀请码。
+ * 主机名(host)用于判断域名是否已被其他代理绑定,以及注册时按访问域名识别代理。
+ */
 class AgentDomainService
 {
     public function save(Agent $agent, array $domains): array
@@ -42,8 +46,9 @@ class AgentDomainService
         return [
             'pc_domain' => $pc,
             'h5_domain' => $h5,
-            'pc_register_url' => $pc === '' ? '' : $this->registerUrl($pc, (string) config('agent.pc_register_path'), $agent->invitation_code),
-            'h5_register_url' => $h5 === '' ? '' : $this->registerUrl($h5, (string) config('agent.h5_register_path'), $agent->invitation_code),
+            // 推广链接 = 绑定的地址本身
+            'pc_register_url' => $pc,
+            'h5_register_url' => $h5,
         ];
     }
 
@@ -61,30 +66,25 @@ class AgentDomainService
         return $domainRow ? Agent::query()->whereKey($domainRow->agent_id)->where('status', Agent::STATUS_ENABLED)->first() : null;
     }
 
+    /** 只去掉首尾空白,其余原样保留;校验能解析出 http/https 主机名。 */
     private function normalize(string $value): string
     {
         $value = trim($value);
         if ($value === '') {
             return '';
         }
-        $candidate = preg_match('#^https?://#i', $value) ? $value : 'https://' . $value;
-        $scheme = strtolower((string) parse_url($candidate, PHP_URL_SCHEME));
-        $host = strtolower((string) parse_url($candidate, PHP_URL_HOST));
-        $port = parse_url($candidate, PHP_URL_PORT);
-        if (!in_array($scheme, ['http', 'https'], true) || $host === '') {
+        if (preg_match('/\s/u', $value) || $this->host($value) === '') {
             throw new \InvalidArgumentException('域名格式错误');
         }
-        return $scheme . '://' . $host . ($port ? ':' . $port : '');
-    }
-
-    private function registerUrl(string $domain, string $path, string $code): string
-    {
-        $separator = str_contains($path, '?') ? '&' : '?';
-        return rtrim($domain, '/') . '/' . ltrim($path, '/') . $separator . 'agent_code=' . urlencode($code);
+        return $value;
     }
 
+    /** 主机名(小写);没写协议时按 https 解析。只允许 http/https。 */
     private function host(string $url): string
     {
-        return strtolower((string) parse_url($url, PHP_URL_HOST));
+        $candidate = preg_match('#^[a-z][a-z0-9+.-]*://#i', $url) ? $url : 'https://' . $url;
+        $scheme = strtolower((string) parse_url($candidate, PHP_URL_SCHEME));
+        if (!in_array($scheme, ['http', 'https'], true)) return '';
+        return strtolower((string) parse_url($candidate, PHP_URL_HOST));
     }
 }

+ 0 - 2
config/agent.php

@@ -6,8 +6,6 @@ return [
         explode(',', (string) env('AGENT_ADMIN_IDS', '1'))
     ))),
     'token_ttl_days' => (int) env('AGENT_TOKEN_TTL_DAYS', 7),
-    'pc_register_path' => env('AGENT_PC_REGISTER_PATH', '/#/user/register'),
-    'h5_register_path' => env('AGENT_H5_REGISTER_PATH', '/#/pages/login/login?type=reg'),
     'online_minutes' => max(1, (int) env('AGENT_ONLINE_MINUTES', 5)),
     'referral_bind_minutes' => max(1, (int) env('AGENT_REFERRAL_BIND_MINUTES', 30)),
 ];

+ 2 - 2
docs/代理/个人信息.md

@@ -58,8 +58,8 @@
     "last_login_ip": "1.2.3.4",
     "pc_domain": "https://pc.example.com",
     "h5_domain": "https://h5.example.com",
-    "pc_register_url": "https://pc.example.com/#/user/register?agent_code=A1B2C3D4E5",
-    "h5_register_url": "https://h5.example.com/#/pages/login/login?type=reg&agent_code=A1B2C3D4E5"
+    "pc_register_url": "https://pc.example.com",
+    "h5_register_url": "https://h5.example.com"
   }
 }
 ```

+ 2 - 2
docs/代理/代理后台调整-前端接入.md

@@ -40,7 +40,7 @@
 | `id`、`username`、`real_name`、`language`、`invitation_code` | 不变 |
 | `type` / `type_text` | `1` 合营 / `2` 入股 |
 | `pc_domain`、`h5_domain` | 总后台分配给代理的推广域名 |
-| `pc_register_url`、`h5_register_url` | **平台邀请地址**(域名 + 注册页 + `agent_code`),没分配域名时为空字符串 |
+| `pc_register_url`、`h5_register_url` | **平台邀请地址**:总后台绑定的地址原样返回(绑定什么就是什么),没分配域名时为空字符串 |
 | `balance` | 总余额 |
 | `frozen_balance` | 提现冻结中 |
 | `withdrawable_amount` | 当前可提现 |
@@ -59,7 +59,7 @@
   "id":12,"username":"agent12","real_name":"","language":"zh","invitation_code":"ABCD123456",
   "type":1,"type_text":"合营","withdraw_fee_rate":"0.0000","has_withdraw_password":true,
   "pc_domain":"https://a.example.com","h5_domain":"https://m.example.com",
-  "pc_register_url":"https://a.example.com/register?agent_code=ABCD123456","h5_register_url":"https://m.example.com/#/register?agent_code=ABCD123456",
+  "pc_register_url":"https://a.example.com","h5_register_url":"https://m.example.com",
   "balance":"21500.0000","frozen_balance":"0.0000","withdrawable_amount":"1500.0000",
   "margin_amount":"20000.0000","margin_balance":"20000.0000","profit_amount":"1500.0000",
   "profit_commission_profile_id":2,"profit_commission_profile":{"id":2,"name":"合营A","margin_amount":"20000.0000","f1_lottery_rate":"10.0000","f1_sport_rate":"10.0000","live_rate":"10.0000","slot_rate":"10.0000","sport_rate":"10.0000","esports_rate":"10.0000","fishing_rate":"10.0000","chess_rate":"10.0000"},

+ 3 - 3
docs/代理/推广域名.md

@@ -18,10 +18,10 @@
   "data": {
     "pc_domain": "https://pc.example.com",
     "h5_domain": "https://h5.example.com",
-    "pc_register_url": "https://pc.example.com/#/user/register?agent_code=A1B2C3D4E5",
-    "h5_register_url": "https://h5.example.com/#/pages/login/login?type=reg&agent_code=A1B2C3D4E5"
+    "pc_register_url": "https://pc.example.com",
+    "h5_register_url": "https://h5.example.com"
   }
 }
 ```
 
-前端复制推广链接时使用返回的 `pc_register_url`/`h5_register_url`,不要自行拼数据库 ID。
+前端复制推广链接时使用返回的 `pc_register_url`/`h5_register_url`。推广链接就是总后台绑定的地址本身(绑定什么就是什么),后端不再拼接注册路径和邀请码;没有绑定域名时为空字符串。

+ 2 - 2
docs/代理/推广注册.md

@@ -80,9 +80,9 @@ Content-Type: application/json
 
 注册接入顺序:
 
-1. 注册页读取 URL 中的 `agent_code`。
+1. 注册页读取 URL 中的 `agent_code`;**没有时把当前页面地址(`location.href`)作为 `domain`**。代理的推广链接就是总后台绑定的域名本身,默认不带 `agent_code`,靠域名识别代理。
 2. 调用本接口验证并取得 `agent_id`。
 3. 完成主站注册并取得用户 Token。
-4. 立即调用 `POST /agent/referral/bind`,由服务端再次验证推广码并写入 `users.agent_id`。
+4. 立即调用 `POST /agent/referral/bind`(同样传 `agent_code` 或 `domain`),由服务端再次验证并写入 `users.agent_id`。
 
 会员绑定代理后不能再修改(总后台的 `/admin/agent/bind-member` 已删除)。公开推广链接使用 `agent_code`,不要公开数据库代理 ID。

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

@@ -2,6 +2,10 @@
 
 本目录是2026-10-26的接口文档交付副本,按**项目 → 前台/后台/代理端**组织。
 
+## 入股配置:代理充值通道(2026-11-09)
+
+- [入股配置-代理充值通道-前端修正单](入股配置-代理充值通道-前端修正单.md):总后台「入股配置」页面新增「代理充值通道组合」下拉,选项来自 `GET /admin/rechargeChannel/groupList`。
+
 ## 入股代理充值冻结(2026-11-09)
 
 - [入股代理充值冻结-前端修正单](入股代理充值冻结-前端修正单.md):入股代理充值的钱 7 天内不能提现,用新字段 `locked_amount`(充值冻结)返回。这笔钱仍包含在 `balance` 里,和 `frozen_balance`(提现冻结)不是一回事,资金页要新增展示。

+ 52 - 0
docs/前端交付/入股配置-代理充值通道-前端修正单.md

@@ -0,0 +1,52 @@
+# 总后台「入股配置」-代理充值通道-前端修正单(2026-11-09)
+
+## 现象
+
+总后台「入股配置」页面看不到充值通道配置,无法设置代理后台能用哪些充值方式。
+
+## 原因
+
+后端已经支持,前端页面还没有加这个字段:`recharge_channel_group_id`(代理充值通道组合)。代理后台的充值方式按这个组合显示,提现方式也用同一个组合。
+
+## 接口(地址不变,新增一个字段)
+
+### 读取 `GET /admin/agent/settings`
+
+返回里新增 `recharge_channel_group_id`:
+
+```json
+{"id":1,"margin_stop_percent":"10.0000","equity_enabled":1,"equity_price_per_percent":"10000.0000",
+ "equity_min_percent":"0.1000","equity_max_percent":"1.0000","recharge_channel_group_id":1,"updated_by":1,"updated_at":"…"}
+```
+
+### 保存 `POST /admin/agent/settings`
+
+原来的参数不变,新增可选参数:
+
+| 参数 | 必填 | 说明 |
+|---|---:|---|
+| `recharge_channel_group_id` | 否 | 通道组合 ID(来自下拉)。不能选已删除的组合;不传则保持原值,传空则改为 `1` |
+
+保存时请把页面上的**全部字段一起提交**(原有的必填字段仍然必填)。
+
+## 页面修改
+
+在「入股配置」表单里新增一项「代理充值通道组合」(下拉):
+
+- 选项来自现有接口 `GET /admin/rechargeChannel/groupList?page=1&limit=100`:
+  - 下拉显示 `data.data[].name`,值为 `data.data[].id`;
+  - 可以在选项下方显示该组合的 `rechargeTypes`(充值方式),方便确认会开放哪些方式。
+- 默认选中 `GET /admin/agent/settings` 返回的 `recharge_channel_group_id`。
+- 字段说明文案:「代理后台的充值方式按此组合显示,所有代理共用,不影响会员。建议新建一个『代理专用』组合,只勾选要开放给代理的方式。」
+
+## 生效规则(给运营看)
+
+- 代理后台能用的充值方式 = 组合里勾选的充值类型 ∩ 已启用的充值通道 ∩ 已启用的支付公司。三方如果配置了「充值白名单用户」,代理看不到该通道。
+- 要关闭代理的某个充值方式又不影响会员:在代理使用的组合里取消勾选即可。
+- 代理充值不送优惠,也不参与任何活动。
+
+## 验收
+
+1. 打开「入股配置」,能看到「代理充值通道组合」下拉,选中的是当前保存的值。
+2. 选一个只勾选「人工充值」的组合并保存,代理后台充值页只显示人工充值。
+3. 换回原来的组合并保存,代理后台充值页恢复原来的充值方式。

+ 7 - 5
docs/总后台/代理列表.md

@@ -77,8 +77,8 @@ GET /admin/agent?page=1&limit=20&username=agent&status=1
         "parent": null,
         "pc_domain": "https://pc.example.com",
         "h5_domain": "https://h5.example.com",
-        "pc_register_url": "https://pc.example.com/#/user/register?agent_code=A1B2C3D4E5",
-        "h5_register_url": "https://h5.example.com/#/pages/login/login?type=reg&agent_code=A1B2C3D4E5"
+        "pc_register_url": "https://pc.example.com",
+        "h5_register_url": "https://h5.example.com"
       }
     ]
   }
@@ -245,7 +245,9 @@ GET /admin/agent/show?id=12
 }
 ```
 
-后端只保存协议、主机、端口,自动去掉输入中的路径和查询参数。一个域名不能绑定给不同代理。
+**绑定什么就保存什么**:输入的地址(包括路径和参数)只去掉首尾空格后原样保存,推广链接 `pc_register_url`/`h5_register_url` 就是绑定的地址本身,后端不再自动拼注册路径和邀请码。需要指向注册页时,直接绑定完整地址,例如 `https://pc.example.com/#/user/register`。
+
+一个域名(按主机名判断,不区分路径)不能绑定给不同代理。会员从这个域名注册时,注册页把当前页面地址作为 `domain` 传给 `/agent/referral/bind`,就能识别代理。
 
 ```json
 {
@@ -255,8 +257,8 @@ GET /admin/agent/show?id=12
   "data": {
     "pc_domain": "https://pc.example.com",
     "h5_domain": "https://h5.example.com",
-    "pc_register_url": "https://pc.example.com/#/user/register?agent_code=A1B2C3D4E5",
-    "h5_register_url": "https://h5.example.com/#/pages/login/login?type=reg&agent_code=A1B2C3D4E5"
+    "pc_register_url": "https://pc.example.com",
+    "h5_register_url": "https://h5.example.com"
   }
 }
 ```

+ 22 - 12
tests/Unit/AgentDomainServiceTest.php

@@ -6,21 +6,31 @@ use App\Services\Agent\AgentDomainService;
 use PHPUnit\Framework\TestCase;
 use ReflectionMethod;
 
+/** 推广域名绑定什么就保存、返回什么;主机名用于唯一性和注册识别。 */
 class AgentDomainServiceTest extends TestCase
 {
-    public function test_hash_router_registration_urls_keep_agent_code_inside_fragment_route(): void
+    private function call(string $method, string $value)
     {
-        $method = new ReflectionMethod(AgentDomainService::class, 'registerUrl');
-        $method->setAccessible(true);
-        $service = new AgentDomainService();
+        $reflection = new ReflectionMethod(AgentDomainService::class, $method);
+        $reflection->setAccessible(true);
+        return $reflection->invoke(new AgentDomainService(), $value);
+    }
 
-        $this->assertSame(
-            'https://pc.example.com/#/user/register?agent_code=ABC123',
-            $method->invoke($service, 'https://pc.example.com', '/#/user/register', 'ABC123')
-        );
-        $this->assertSame(
-            'https://h5.example.com/#/pages/login/login?type=reg&agent_code=ABC123',
-            $method->invoke($service, 'https://h5.example.com', '/#/pages/login/login?type=reg', 'ABC123')
-        );
+    public function test_bound_value_is_kept_exactly_and_host_is_extracted(): void
+    {
+        foreach ([
+            'https://pc.example.com' => 'pc.example.com',
+            '  https://pc.example.com/#/user/register?ref=A1  ' => 'pc.example.com',
+            'h5.Example.com/reg' => 'h5.example.com',
+            'http://pc.example.com:8080/x' => 'pc.example.com',
+        ] as $input => $host) {
+            $this->assertSame(trim($input), $this->call('normalize', $input), 'saved exactly as entered (only trimmed)');
+            $this->assertSame($host, $this->call('host', trim($input)));
+        }
+        $this->assertSame('', $this->call('normalize', '   '));
+        foreach (['ftp://pc.example.com', 'https://', 'pc example.com'] as $bad) {
+            try { $this->call('normalize', $bad); $this->fail($bad); }
+            catch (\InvalidArgumentException $e) { $this->assertSame('域名格式错误', $e->getMessage()); }
+        }
     }
 }