doge 10 часов назад
Родитель
Сommit
5f44f43b04

+ 2 - 1
app/Http/Controllers/admin/Agent.php

@@ -274,7 +274,8 @@ class Agent extends AgentApiController
                 : ['required', 'string', 'min:4', 'max:64', 'alpha_dash', 'unique:agents,username'],
             'password' => [$update ? 'nullable' : 'required', 'string', 'min:6', 'max:100'],
             'withdraw_password' => ['nullable', 'string', 'min:6', 'max:100'], 'real_name' => ['nullable', 'string', 'max:128'],
-            'parent_id' => ['nullable', 'integer', 'exists:agents,id'], 'deposit_fee_rate' => ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],
+            'parent_id' => $update ? ['nullable', 'integer', 'exists:agents,id'] : ['prohibited'],
+            'deposit_fee_rate' => ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],
             'withdraw_fee_rate' => ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],
             'flow_commission_rate' => $update ? ['prohibited'] : ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],
             'profit_commission_rate' => $update ? ['prohibited'] : ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],

+ 3 - 0
app/Http/Controllers/agent/AuthController.php

@@ -22,6 +22,9 @@ class AuthController extends AgentApiController
                 'username' => $agent->username,
                 'real_name' => $agent->real_name,
                 'invitation_code' => $agent->invitation_code,
+                'level' => (int) $agent->level,
+                'level_text' => $agent->level_text,
+                'can_create_sub_agent' => $agent->can_create_sub_agent,
             ];
             return $result;
         });

+ 3 - 0
app/Http/Controllers/agent/ProfileController.php

@@ -23,6 +23,9 @@ class ProfileController extends AgentApiController
                 'real_name' => $agent->real_name,
                 'parent_id' => $agent->parent_id,
                 'parent_username' => $agent->parent?->username,
+                'level' => (int) $agent->level,
+                'level_text' => $agent->level_text,
+                'can_create_sub_agent' => $agent->can_create_sub_agent,
                 'invitation_code' => $agent->invitation_code,
                 'balance' => (string) $agent->balance,
                 'frozen_balance' => (string) $agent->frozen_balance,

+ 1 - 1
app/Http/Controllers/agent/SubAgentController.php

@@ -49,7 +49,7 @@ class SubAgentController extends AgentApiController
                 'password' => ['required', 'string', 'min:6', 'max:100'],
                 'withdraw_password' => ['nullable', 'string', 'min:6', 'max:100'],
                 'real_name' => ['nullable', 'string', 'max:128'],
-                'parent_id' => ['nullable', 'integer', 'exists:agents,id'],
+                'parent_id' => ['prohibited'],
                 'deposit_fee_rate' => ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],
                 'withdraw_fee_rate' => ['nullable', 'numeric', 'between:0,100', new DecimalNumber(3, 4)],
                 'flow_commission_rate' => ['nullable', 'numeric', 'between:0,' . $root->flow_commission_rate, new DecimalNumber(3, 4)],

+ 14 - 0
app/Models/Agent/Agent.php

@@ -9,6 +9,9 @@ class Agent extends BaseModel
 {
     public const STATUS_DISABLED = 0;
     public const STATUS_ENABLED = 1;
+    public const LEVEL_ONE = 1;
+    public const LEVEL_TWO = 2;
+    public const MAX_LEVEL = self::LEVEL_TWO;
 
     protected $table = 'agents';
     protected $fillable = [
@@ -18,6 +21,7 @@ class Agent extends BaseModel
         'profit_commission_rate', 'status', 'last_login_at', 'last_login_ip', 'created_by',
     ];
     protected $hidden = ['password', 'withdraw_password'];
+    protected $appends = ['level_text', 'can_create_sub_agent'];
     protected $casts = [
         'parent_id' => 'integer', 'level' => 'integer', 'status' => 'integer',
         'balance' => 'decimal:4', 'frozen_balance' => 'decimal:4',
@@ -50,4 +54,14 @@ class Agent extends BaseModel
     {
         return $this->hasMany(User::class, 'agent_id');
     }
+
+    public function getLevelTextAttribute(): string
+    {
+        return (int) $this->level === self::LEVEL_ONE ? '一级代理' : '二级代理';
+    }
+
+    public function getCanCreateSubAgentAttribute(): bool
+    {
+        return (int) $this->level === self::LEVEL_ONE;
+    }
 }

+ 3 - 1
app/Services/Agent/AgentReportService.php

@@ -20,7 +20,9 @@ class AgentReportService
                     $query->whereKey($root->id)
                         ->orWhere('path', 'like', rtrim((string) $root->path, '/') . '/%');
                 });
-            }, fn($query) => $query->where('path', 'like', rtrim((string) $root->path, '/') . '/%'))
+            }, fn($query) => $query
+                ->where('id', '<>', $root->id)
+                ->where('path', 'like', rtrim((string) $root->path, '/') . '/%'))
             ->pluck('id')->map(fn($id) => (int) $id)->all();
     }
 

+ 23 - 3
app/Services/Agent/AgentService.php

@@ -15,8 +15,15 @@ class AgentService
         return DB::transaction(function () use ($data, $createdBy, $scopeAgent): Agent {
             $parentId = isset($data['parent_id']) ? (int) $data['parent_id'] : null;
             if ($scopeAgent) {
-                $parentId = $parentId ?: (int) $scopeAgent->id;
-                $this->assertVisible($scopeAgent, $parentId);
+                if ((int) $scopeAgent->level !== Agent::LEVEL_ONE) {
+                    throw new \RuntimeException('二级代理不能继续新增下级代理');
+                }
+                if ($parentId && $parentId !== (int) $scopeAgent->id) {
+                    throw new \RuntimeException('一级代理只能新增自己的直属二级代理');
+                }
+                $parentId = (int) $scopeAgent->id;
+            } elseif ($parentId) {
+                throw new \RuntimeException('总后台只能新增一级代理');
             }
             $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null;
             if ($scopeAgent && $parent && (int) $parent->status !== Agent::STATUS_ENABLED) {
@@ -25,11 +32,15 @@ class AgentService
             if ($parent) {
                 $this->assertRatesWithinParent($parent, $data);
             }
+            $level = $parent ? ((int) $parent->level + 1) : Agent::LEVEL_ONE;
+            if ($level > Agent::MAX_LEVEL) {
+                throw new \RuntimeException('代理层级最多为二级');
+            }
 
             $agent = Agent::query()->create([
                 'parent_id' => $parent?->id,
                 'path' => '/',
-                'level' => $parent ? ((int) $parent->level + 1) : 1,
+                'level' => $level,
                 'username' => trim((string) $data['username']),
                 'password' => Hash::make((string) $data['password']),
                 'withdraw_password' => empty($data['withdraw_password']) ? '' : Hash::make((string) $data['withdraw_password']),
@@ -142,6 +153,9 @@ class AgentService
         if ($parent && str_starts_with((string) $parent->path, (string) $agent->path)) {
             throw new \RuntimeException('不能把代理移动到自己的下级');
         }
+        if ($parent && (int) $parent->level !== Agent::LEVEL_ONE) {
+            throw new \RuntimeException('二级代理不能作为上级代理');
+        }
         if ($parent) {
             $this->assertRatesWithinParent($parent, [
                 'flow_commission_rate' => $agent->flow_commission_rate,
@@ -153,6 +167,12 @@ class AgentService
         $newLevel = $parent ? ((int) $parent->level + 1) : 1;
         $newPath = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/';
         $descendants = Agent::query()->where('path', 'like', rtrim($oldPath, '/') . '/%')->lockForUpdate()->get();
+        $newMaxLevel = $descendants->max(fn(Agent $descendant) =>
+            (int) $descendant->level + $newLevel - $oldLevel
+        ) ?? $newLevel;
+        if ($newLevel > Agent::MAX_LEVEL || $newMaxLevel > Agent::MAX_LEVEL) {
+            throw new \RuntimeException('移动后会产生三级代理,操作已拒绝');
+        }
 
         $agent->parent_id = $parent?->id;
         $agent->path = $newPath;

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

@@ -19,6 +19,9 @@
     "real_name": "张三",
     "parent_id": null,
     "parent_username": null,
+    "level": 1,
+    "level_text": "一级代理",
+    "can_create_sub_agent": true,
     "invitation_code": "A1B2C3D4E5",
     "balance": "5500.0000",
     "frozen_balance": "200.0000",

+ 5 - 2
docs/代理/所有代理.md

@@ -33,6 +33,8 @@ GET /agent/sub-agents?page=1&limit=20&direct_only=1
         "parent_id": 12,
         "path": "/12/15/",
         "level": 2,
+        "level_text": "二级代理",
+        "can_create_sub_agent": false,
         "username": "sub001",
         "real_name": "李四",
         "invitation_code": "F6G7H8J9K0",
@@ -60,13 +62,14 @@ GET /agent/sub-agents?page=1&limit=20&direct_only=1
 
 ### `POST /agent/sub-agents`
 
+只有一级代理可以调用;新账号固定成为当前代理的直属二级代理。二级代理调用会返回业务错误。
+
 | 参数 | 类型 | 必填 | 说明 |
 |---|---|---:|---|
 | `username` | string | 是 | 4~64 位,字母/数字/横线/下划线,全局唯一 |
 | `password` | string | 是 | 6~100 位 |
 | `withdraw_password` | string | 否 | 6~100 位 |
 | `real_name` | string | 否 | 最长 128 |
-| `parent_id` | integer | 否 | 不传时为当前代理;传值时必须是当前代理或其下级 |
 | `deposit_fee_rate` | decimal string | 否 | 0~100 |
 | `withdraw_fee_rate` | decimal string | 否 | 0~100 |
 | `flow_commission_rate` | decimal string | 否 | 不能高于上级流水比例 |
@@ -85,7 +88,7 @@ GET /agent/sub-agents?page=1&limit=20&direct_only=1
 }
 ```
 
-成功时 `data` 返回新代理对象,包括后端生成的 `id`、`path`、`level`、`invitation_code`,初始 `balance` 和 `frozen_balance` 都为 `0.0000`。
+前端不要传 `parent_id`。成功时 `data` 返回新代理对象,包括后端生成的 `id`、`path`、`level=2`、`invitation_code`,初始 `balance` 和 `frozen_balance` 都为 `0.0000`。
 
 ## 3. 修改下级代理
 

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

@@ -1,6 +1,7 @@
 # 代理后台-推广注册接入
 
 该文件供主站注册页使用,不需要代理 Token。
+一级代理和二级代理都有独立推广码,会员会绑定到实际推广自己的那一级代理。
 
 ## 解析推广码或域名
 

+ 4 - 1
docs/代理/登录.md

@@ -30,7 +30,10 @@
       "id": 12,
       "username": "agent001",
       "real_name": "张三",
-      "invitation_code": "A1B2C3D4E5"
+      "invitation_code": "A1B2C3D4E5",
+      "level": 1,
+      "level_text": "一级代理",
+      "can_create_sub_agent": true
     }
   }
 }

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

@@ -49,6 +49,8 @@ GET /admin/agent?page=1&limit=20&username=agent&status=1
         "parent_id": null,
         "path": "/12/",
         "level": 1,
+        "level_text": "一级代理",
+        "can_create_sub_agent": true,
         "username": "agent001",
         "real_name": "张三",
         "invitation_code": "A1B2C3D4E5",
@@ -117,13 +119,14 @@ GET /admin/agent/show?id=12
 
 ### `POST /admin/agent/create`
 
+总后台新增的账号固定为一级代理,不能指定 `parent_id`。
+
 | 参数 | 类型 | 必填 | 规则/说明 |
 |---|---|---:|---|
 | `username` | string | 是 | 4~64 位,只允许字母、数字、横线、下划线,全局唯一 |
 | `password` | string | 是 | 6~100 位 |
 | `withdraw_password` | string | 否 | 提现密码,6~100 位;不传时代理之后自行设置 |
 | `real_name` | string | 否 | 真实姓名,最长 128 |
-| `parent_id` | integer/null | 否 | 上级代理 ID;空表示一级代理 |
 | `initial_balance` | decimal string | 否 | 初始站内额度,最小 0,最多 14 位整数和 4 位小数 |
 | `deposit_fee_rate` | decimal string | 否 | 存款手续费百分比,0~100,最多 4 位小数 |
 | `withdraw_fee_rate` | decimal string | 否 | 提现手续费百分比,0~100,最多 4 位小数 |
@@ -137,7 +140,6 @@ GET /admin/agent/show?id=12
   "password": "123456",
   "withdraw_password": "654321",
   "real_name": "张三",
-  "parent_id": null,
   "initial_balance": "5000.0000",
   "deposit_fee_rate": "0",
   "withdraw_fee_rate": "1.5",
@@ -180,7 +182,7 @@ GET /admin/agent/show?id=12
 | `password` | string | 否 | 新登录密码,6~100 位;空值不修改 |
 | `withdraw_password` | string | 否 | 新提现密码,6~100 位;空值不修改 |
 | `real_name` | string | 否 | 最长 128 |
-| `parent_id` | integer/null | 否 | 修改上级;`null` 移到一级,后端会同步整棵下级路径并阻止循环层级 |
+| `parent_id` | integer/null | 否 | 管理员调整层级;`null` 移到一级,只能把无下级代理的账号放到一级代理下面,且永远不能产生三级代理 |
 | `deposit_fee_rate` | decimal string | 否 | 0~100 |
 | `withdraw_fee_rate` | decimal string | 否 | 0~100 |
 | `status` | integer | 否 | `1` 启用,`0` 禁用 |