Forráskód Böngészése

增加域名绑定队员获取分润功能

wilhelm wong 1 hónapja
szülő
commit
3341903330
43 módosított fájl, 4570 hozzáadás és 712 törlés
  1. 148 0
      deploy-enhanced.sh
  2. 524 0
      docs/page-click-record-api.md
  3. 2 1
      package.json
  4. 292 0
      scripts/generate-commission-index.ts
  5. 4 0
      src/app.ts
  6. 25 2
      src/controllers/income-records.controller.ts
  7. 219 0
      src/controllers/landing-domain-pool.controller.ts
  8. 6 5
      src/controllers/member.controller.ts
  9. 160 0
      src/controllers/page-click-record.controller.ts
  10. 27 60
      src/controllers/team-domain.controller.ts
  11. 6 1
      src/controllers/user.controller.ts
  12. 33 0
      src/dto/landing-domain-pool.dto.ts
  13. 1 0
      src/dto/member.dto.ts
  14. 30 0
      src/dto/page-click-record.dto.ts
  15. 59 15
      src/dto/team-members.dto.ts
  16. 49 0
      src/entities/agent-commission-index.entity.ts
  17. 2 2
      src/entities/banner-daily-statistics.entity.ts
  18. 2 2
      src/entities/banner.entity.ts
  19. 2 2
      src/entities/finance.entity.ts
  20. 6 3
      src/entities/income-records.entity.ts
  21. 38 0
      src/entities/landing-domain-pool.entity.ts
  22. 3 3
      src/entities/member.entity.ts
  23. 1 1
      src/entities/promotion-link.entity.ts
  24. 1 1
      src/entities/sys-config.entity.ts
  25. 1 1
      src/entities/team-domain.entity.ts
  26. 3 0
      src/entities/team-members.entity.ts
  27. 2 2
      src/entities/user-invite-record.entity.ts
  28. 3 2
      src/entities/user.entity.ts
  29. 76 0
      src/routes/landing-domain-pool.routes.ts
  30. 1 1
      src/routes/member.routes.ts
  31. 42 0
      src/routes/page-click-record.routes.ts
  32. 90 26
      src/services/income-records.service.ts
  33. 386 0
      src/services/landing-domain-pool.service.ts
  34. 187 13
      src/services/member.service.ts
  35. 664 0
      src/services/multi-level-commission.service.ts
  36. 187 0
      src/services/page-click-record.service.ts
  37. 106 139
      src/services/payment.service.ts
  38. 80 19
      src/services/sys-config.service.ts
  39. 13 10
      src/services/team-domain.service.ts
  40. 634 142
      src/services/team-members.service.ts
  41. 33 28
      src/services/team.service.ts
  42. 170 25
      src/services/user.service.ts
  43. 252 206
      yarn.lock

+ 148 - 0
deploy-enhanced.sh

@@ -0,0 +1,148 @@
+#!/bin/bash
+# Enhanced Deployment Script for junma-api
+# 参考PowerShell脚本结构,实现bash发布命令
+
+set -e
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
+
+# 日志函数
+log_info() {
+    echo -e "${GREEN}$1${NC}"
+}
+
+log_warn() {
+    echo -e "${YELLOW}$1${NC}"
+}
+
+log_error() {
+    echo -e "${RED}$1${NC}"
+}
+
+log_step() {
+    echo -e "${CYAN}$1${NC}"
+}
+
+# 错误处理函数
+handle_error() {
+    log_error "部署失败: $1"
+    exit 1
+}
+
+# 主部署流程
+log_info "开始 junma-api 部署..."
+
+try {
+    # 拉取最新代码
+    log_warn "拉取最新代码..."
+    if ! git pull origin main; then
+        handle_error "Git pull 失败"
+    fi
+
+    # 清理旧的构建文件
+    log_warn "清理旧的构建文件..."
+    if [ -d "dist" ]; then
+        rm -rf dist
+    fi
+
+    # 清理node_modules(可选)
+    if [ -d "node_modules" ]; then
+        log_warn "清理node_modules..."
+        rm -rf node_modules
+    fi
+
+    # 移除锁文件(可选)
+    if [ -f "package-lock.json" ]; then
+        rm -f package-lock.json
+    fi
+    if [ -f "yarn.lock" ]; then
+        rm -f yarn.lock
+    fi
+
+    # 安装依赖
+    log_warn "安装依赖..."
+    if ! yarn install; then
+        handle_error "依赖安装失败"
+    fi
+
+    # 构建生产版本
+    log_warn "构建生产版本..."
+    if ! yarn build; then
+        handle_error "构建失败"
+    fi
+
+    # 检查构建结果
+    log_warn "检查构建结果..."
+    if [ ! -d "dist" ]; then
+        handle_error "构建失败,dist目录不存在"
+    fi
+
+    # 复制环境配置文件
+    log_warn "复制环境配置文件..."
+    if [ -f ".env.production" ]; then
+        cp .env.production dist/.env
+    else
+        log_warn "警告: .env.production 文件不存在"
+    fi
+
+    # 复制package.json
+    cp package.json dist/package.json
+
+    # 部署到服务器
+    log_warn "部署到服务器..."
+    
+    # 尝试不同的上传方法
+    upload_success=false
+    
+    # 尝试rsync
+    if command -v rsync >/dev/null 2>&1; then
+        log_step "使用rsync上传..."
+        if rsync --exclude='node_modules/' -ravzh --delete -e "ssh -o StrictHostKeyChecking=no" ./dist/ root@8.210.167.152:/var/www/junma-api/; then
+            upload_success=true
+            log_info "rsync上传成功"
+        else
+            log_warn "rsync上传失败,尝试scp..."
+        fi
+    else
+        log_warn "rsync不可用,尝试scp..."
+    fi
+    
+    # 尝试scp如果rsync失败
+    if [ "$upload_success" = false ]; then
+        if command -v scp >/dev/null 2>&1; then
+            log_step "使用scp上传..."
+            if scp -r -o StrictHostKeyChecking=no ./dist/* root@8.210.167.152:/var/www/junma-api/; then
+                upload_success=true
+                log_info "scp上传成功"
+            else
+                handle_error "scp上传失败"
+            fi
+        else
+            handle_error "没有可用的上传工具 (rsync/scp)"
+        fi
+    fi
+    
+    if [ "$upload_success" = false ]; then
+        handle_error "文件上传失败 - 没有找到可用的上传方法"
+    fi
+
+    # 在服务器上执行部署后操作
+    log_warn "在服务器上执行部署后操作..."
+    if ssh -o StrictHostKeyChecking=no root@8.210.167.152 "cd /var/www/junma-api && yarn install && pm2 restart junma-api"; then
+        log_info "服务器部署操作成功"
+    else
+        handle_error "服务器部署操作失败"
+    fi
+
+    log_info "部署完成!"
+    log_info "API服务已重启"
+    log_info "服务地址: http://8.210.167.152:3000"
+} catch {
+    log_error "部署过程中发生错误: $1"
+    exit 1
+}

+ 524 - 0
docs/page-click-record-api.md

@@ -0,0 +1,524 @@
+# 页面点击记录 API 接口文档
+
+## 目录
+- [概述](#概述)
+- [基础信息](#基础信息)
+- [数据模型](#数据模型)
+- [接口列表](#接口列表)
+  - [1. 记录页面点击](#1-记录页面点击)
+  - [2. 获取页面点击统计](#2-获取页面点击统计)
+  - [3. 获取今日点击统计汇总](#3-获取今日点击统计汇总)
+  - [4. 获取指定页面的点击量](#4-获取指定页面的点击量)
+- [错误码说明](#错误码说明)
+- [使用示例](#使用示例)
+
+---
+
+## 概述
+
+页面点击记录功能用于统计首页和视频页面的访问量。数据存储在 Redis 中,自动保存 7 天,支持按 IP 去重(同一 IP 每天只计数一次)。
+
+**支持的页面类型:**
+- `home` - 首页
+- `video` - 视频页面
+
+---
+
+## 基础信息
+
+**Base URL:** `/api/page-clicks`
+
+**认证方式:**
+- 记录接口:无需认证(公开接口)
+- 查询接口:需要管理员权限(Bearer Token)
+
+**数据存储:**
+- 存储位置:Redis
+- 保留时间:7 天(自动过期)
+- 去重机制:同一 IP 每天只计数一次
+
+---
+
+## 数据模型
+
+### PageType(页面类型枚举)
+
+```typescript
+enum PageType {
+  HOME = 'home',    // 首页
+  VIDEO = 'video'   // 视频页面
+}
+```
+
+### 请求/响应数据结构
+
+#### 记录点击请求
+```json
+{
+  "pageType": "home"  // 或 "video"
+}
+```
+
+#### 统计响应
+```json
+{
+  "success": true,
+  "statistics": [
+    {
+      "pageType": "home",
+      "date": "2024-01-15",
+      "clickCount": 1234
+    }
+  ],
+  "total": [
+    {
+      "pageType": "home",
+      "totalClicks": 5678
+    }
+  ]
+}
+```
+
+---
+
+## 接口列表
+
+### 1. 记录页面点击
+
+记录用户访问首页或视频页面的点击。
+
+**接口地址:** `POST /api/page-clicks/click`
+
+**认证要求:** 无需认证(公开接口)
+
+**请求头:**
+```
+Content-Type: application/json
+```
+
+**请求参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| pageType | string | 是 | 页面类型,必须是 `"home"` 或 `"video"` |
+
+**请求示例:**
+```bash
+curl -X POST http://your-domain.com/api/page-clicks/click \
+  -H "Content-Type: application/json" \
+  -d '{
+    "pageType": "home"
+  }'
+```
+
+**响应示例:**
+
+成功响应(200):
+```json
+{
+  "success": true,
+  "pageType": "home",
+  "clickCount": 1234,
+  "message": "点击记录成功"
+}
+```
+
+错误响应(400):
+```json
+{
+  "message": "pageType 为必填字段,必须是 \"home\" 或 \"video\""
+}
+```
+
+错误响应(503):
+```json
+{
+  "message": "Redis服务未配置,无法记录点击"
+}
+```
+
+**说明:**
+- 系统会自动获取客户端 IP 进行去重
+- 同一 IP 在同一天多次访问同一页面,只计数一次
+- 点击量实时更新
+
+---
+
+### 2. 获取页面点击统计
+
+获取指定日期范围内的页面点击统计数据。
+
+**接口地址:** `GET /api/page-clicks/statistics`
+
+**认证要求:** 需要管理员权限(Bearer Token)
+
+**请求头:**
+```
+Authorization: Bearer {token}
+```
+
+**查询参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| pageType | string | 否 | 页面类型,`"home"` 或 `"video"`,不传则返回所有页面 |
+| startDate | string | 否 | 开始日期,格式:`YYYY-MM-DD`,默认:7天前 |
+| endDate | string | 否 | 结束日期,格式:`YYYY-MM-DD`,默认:今天 |
+
+**请求示例:**
+
+查询所有页面最近7天的统计:
+```bash
+curl -X GET "http://your-domain.com/api/page-clicks/statistics" \
+  -H "Authorization: Bearer {token}"
+```
+
+查询首页指定日期范围:
+```bash
+curl -X GET "http://your-domain.com/api/page-clicks/statistics?pageType=home&startDate=2024-01-01&endDate=2024-01-07" \
+  -H "Authorization: Bearer {token}"
+```
+
+**响应示例:**
+
+成功响应(200):
+```json
+{
+  "success": true,
+  "statistics": [
+    {
+      "pageType": "home",
+      "date": "2024-01-15",
+      "clickCount": 1234
+    },
+    {
+      "pageType": "home",
+      "date": "2024-01-16",
+      "clickCount": 1456
+    },
+    {
+      "pageType": "video",
+      "date": "2024-01-15",
+      "clickCount": 2345
+    },
+    {
+      "pageType": "video",
+      "date": "2024-01-16",
+      "clickCount": 2678
+    }
+  ],
+  "total": [
+    {
+      "pageType": "home",
+      "totalClicks": 2690
+    },
+    {
+      "pageType": "video",
+      "totalClicks": 5023
+    }
+  ],
+  "query": {
+    "pageType": "all",
+    "startDate": "2024-01-15",
+    "endDate": "2024-01-16"
+  }
+}
+```
+
+错误响应(400):
+```json
+{
+  "message": "pageType 必须是 \"home\" 或 \"video\""
+}
+```
+
+或
+
+```json
+{
+  "message": "日期格式错误,请使用 YYYY-MM-DD 格式"
+}
+```
+
+**说明:**
+- 统计数据按日期和页面类型排序
+- `statistics` 数组包含每日详细数据
+- `total` 数组包含每个页面的总点击量
+- 如果查询日期范围内没有数据,返回空数组
+
+---
+
+### 3. 获取今日点击统计汇总
+
+获取所有页面今日的点击统计汇总。
+
+**接口地址:** `GET /api/page-clicks/statistics/today`
+
+**认证要求:** 需要管理员权限(Bearer Token)
+
+**请求头:**
+```
+Authorization: Bearer {token}
+```
+
+**请求示例:**
+```bash
+curl -X GET "http://your-domain.com/api/page-clicks/statistics/today" \
+  -H "Authorization: Bearer {token}"
+```
+
+**响应示例:**
+
+成功响应(200):
+```json
+{
+  "success": true,
+  "date": "2024-01-16",
+  "summary": [
+    {
+      "pageType": "video",
+      "clickCount": 1234
+    },
+    {
+      "pageType": "home",
+      "clickCount": 567
+    }
+  ]
+}
+```
+
+**说明:**
+- 返回结果按点击量降序排序
+- `date` 字段为当前日期
+- 即使点击量为 0 的页面也会返回
+
+---
+
+### 4. 获取指定页面的点击量
+
+获取指定页面在指定日期的点击量。
+
+**接口地址:** `GET /api/page-clicks/count`
+
+**认证要求:** 需要管理员权限(Bearer Token)
+
+**请求头:**
+```
+Authorization: Bearer {token}
+```
+
+**查询参数:**
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| pageType | string | 是 | 页面类型,必须是 `"home"` 或 `"video"` |
+| date | string | 否 | 日期,格式:`YYYY-MM-DD`,默认:今天 |
+
+**请求示例:**
+
+查询首页今日点击量:
+```bash
+curl -X GET "http://your-domain.com/api/page-clicks/count?pageType=home" \
+  -H "Authorization: Bearer {token}"
+```
+
+查询视频页面指定日期点击量:
+```bash
+curl -X GET "http://your-domain.com/api/page-clicks/count?pageType=video&date=2024-01-15" \
+  -H "Authorization: Bearer {token}"
+```
+
+**响应示例:**
+
+成功响应(200):
+```json
+{
+  "success": true,
+  "pageType": "home",
+  "date": "2024-01-16",
+  "clickCount": 1234
+}
+```
+
+错误响应(400):
+```json
+{
+  "message": "pageType 为必填字段,必须是 \"home\" 或 \"video\""
+}
+```
+
+或
+
+```json
+{
+  "message": "日期格式错误,请使用 YYYY-MM-DD 格式"
+}
+```
+
+**说明:**
+- 如果指定日期没有数据,`clickCount` 返回 `0`
+- 只能查询最近 7 天内的数据(超过 7 天的数据已过期)
+
+---
+
+## 错误码说明
+
+| HTTP 状态码 | 说明 | 示例 |
+|------------|------|------|
+| 200 | 请求成功 | - |
+| 400 | 请求参数错误 | 页面类型错误、日期格式错误等 |
+| 401 | 未认证 | 缺少或无效的 Token |
+| 403 | 权限不足 | 非管理员用户访问管理接口 |
+| 500 | 服务器内部错误 | 服务器处理异常 |
+| 503 | 服务不可用 | Redis 未配置或连接失败 |
+
+**错误响应格式:**
+```json
+{
+  "message": "错误描述信息"
+}
+```
+
+---
+
+## 使用示例
+
+### JavaScript/TypeScript 示例
+
+#### 记录首页点击
+```javascript
+async function recordHomeClick() {
+  const response = await fetch('http://your-domain.com/api/page-clicks/click', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({
+      pageType: 'home'
+    })
+  });
+  
+  const data = await response.json();
+  console.log('点击记录成功,当前点击量:', data.clickCount);
+}
+```
+
+#### 记录视频页面点击
+```javascript
+async function recordVideoClick() {
+  const response = await fetch('http://your-domain.com/api/page-clicks/click', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    body: JSON.stringify({
+      pageType: 'video'
+    })
+  });
+  
+  const data = await response.json();
+  console.log('点击记录成功,当前点击量:', data.clickCount);
+}
+```
+
+#### 获取统计数据(需要管理员 Token)
+```javascript
+async function getStatistics(pageType, startDate, endDate) {
+  const token = 'your-admin-token';
+  const params = new URLSearchParams();
+  
+  if (pageType) params.append('pageType', pageType);
+  if (startDate) params.append('startDate', startDate);
+  if (endDate) params.append('endDate', endDate);
+  
+  const response = await fetch(
+    `http://your-domain.com/api/page-clicks/statistics?${params}`,
+    {
+      headers: {
+        'Authorization': `Bearer ${token}`
+      }
+    }
+  );
+  
+  const data = await response.json();
+  console.log('统计数据:', data);
+  return data;
+}
+
+// 使用示例
+getStatistics('home', '2024-01-01', '2024-01-07');
+```
+
+#### 获取今日汇总
+```javascript
+async function getTodaySummary() {
+  const token = 'your-admin-token';
+  const response = await fetch(
+    'http://your-domain.com/api/page-clicks/statistics/today',
+    {
+      headers: {
+        'Authorization': `Bearer ${token}`
+      }
+    }
+  );
+  
+  const data = await response.json();
+  console.log('今日汇总:', data.summary);
+  return data;
+}
+```
+
+### cURL 示例
+
+#### 记录点击
+```bash
+# 记录首页点击
+curl -X POST http://your-domain.com/api/page-clicks/click \
+  -H "Content-Type: application/json" \
+  -d '{"pageType": "home"}'
+
+# 记录视频页面点击
+curl -X POST http://your-domain.com/api/page-clicks/click \
+  -H "Content-Type: application/json" \
+  -d '{"pageType": "video"}'
+```
+
+#### 查询统计
+```bash
+# 获取所有页面最近7天统计
+curl -X GET "http://your-domain.com/api/page-clicks/statistics" \
+  -H "Authorization: Bearer YOUR_TOKEN"
+
+# 获取首页指定日期范围统计
+curl -X GET "http://your-domain.com/api/page-clicks/statistics?pageType=home&startDate=2024-01-01&endDate=2024-01-07" \
+  -H "Authorization: Bearer YOUR_TOKEN"
+
+# 获取今日汇总
+curl -X GET "http://your-domain.com/api/page-clicks/statistics/today" \
+  -H "Authorization: Bearer YOUR_TOKEN"
+
+# 获取指定页面指定日期点击量
+curl -X GET "http://your-domain.com/api/page-clicks/count?pageType=video&date=2024-01-15" \
+  -H "Authorization: Bearer YOUR_TOKEN"
+```
+
+---
+
+## 注意事项
+
+1. **数据保留时间**:所有数据在 Redis 中保存 7 天,超过 7 天的数据会自动过期删除
+2. **IP 去重**:同一 IP 在同一天访问同一页面多次,只计数一次
+3. **时区**:所有日期使用服务器时区,按天(00:00:00 - 23:59:59)统计
+4. **性能**:记录接口无需认证,适合高频调用;查询接口需要管理员权限
+5. **Redis 依赖**:功能依赖 Redis,如果 Redis 未配置或连接失败,记录接口会返回 503 错误
+
+---
+
+## 更新日志
+
+- **v1.0.0** (2024-01-16)
+  - 初始版本
+  - 支持首页和视频页面点击记录
+  - 支持 7 天数据保留
+  - 支持 IP 去重
+

+ 2 - 1
package.json

@@ -8,7 +8,8 @@
     "start": "node dist/server.js",
     "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
     "typeorm": "typeorm-ts-node-commonjs",
-    "test": "node --env-file=.env.test ./node_modules/mocha/bin/mocha --allow-uncaught"
+    "test": "node --env-file=.env.test ./node_modules/mocha/bin/mocha --allow-uncaught",
+    "generate-commission-index": "ts-node scripts/generate-commission-index.ts"
   },
   "dependencies": {
     "@fastify/cors": "^11.0.1",

+ 292 - 0
scripts/generate-commission-index.ts

@@ -0,0 +1,292 @@
+/**
+ * 批量生成分润索引表记录脚本
+ * 
+ * 功能:
+ * 1. 如果 commissionDetails 有数据,直接使用
+ * 2. 如果 commissionDetails 没有数据,根据字段生成2层分润:
+ *    - 第1层:agentId 和 incomeAmount
+ *    - 第2层:personalAgentId 和 personalIncomeAmount
+ */
+
+import { DataSource } from 'typeorm'
+import { IncomeRecords } from '../src/entities/income-records.entity'
+import { AgentCommissionIndex } from '../src/entities/agent-commission-index.entity'
+import { CommissionDetail } from '../src/services/multi-level-commission.service'
+import { createApp } from '../src/app'
+
+interface MigrationStats {
+  total: number
+  processed: number
+  success: number
+  failed: number
+  skipped: number
+  withDetails: number
+  withoutDetails: number
+}
+
+class CommissionIndexGenerator {
+  private dataSource: DataSource
+  private stats: MigrationStats = {
+    total: 0,
+    processed: 0,
+    success: 0,
+    failed: 0,
+    skipped: 0,
+    withDetails: 0,
+    withoutDetails: 0
+  }
+
+  constructor(dataSource: DataSource) {
+    this.dataSource = dataSource
+  }
+
+  /**
+   * 从 commissionDetails JSON 字符串解析分润明细
+   */
+  private parseCommissionDetails(commissionDetails: string | null): CommissionDetail[] | null {
+    if (!commissionDetails) {
+      return null
+    }
+
+    try {
+      const details = JSON.parse(commissionDetails)
+      if (Array.isArray(details) && details.length > 0) {
+        return details as CommissionDetail[]
+      }
+      return null
+    } catch (error) {
+      console.error('解析 commissionDetails 失败:', error)
+      return null
+    }
+  }
+
+  /**
+   * 根据字段生成2层分润明细
+   */
+  private generateCommissionDetailsFromFields(
+    record: IncomeRecords
+  ): CommissionDetail[] {
+    const details: CommissionDetail[] = []
+
+    // 第1层:agentId 和 incomeAmount
+    if (record.agentId && record.agentId > 0 && record.incomeAmount && Number(record.incomeAmount) > 0) {
+      const orderPrice = Number(record.orderPrice) || 0
+      const incomeAmount = Number(record.incomeAmount)
+      // 计算分润比例
+      const rate = orderPrice > 0 ? (incomeAmount / orderPrice) * 100 : 0
+
+      details.push({
+        level: 1,
+        agentId: record.agentId,
+        rate: Number(rate.toFixed(2)),
+        amount: incomeAmount
+      })
+    }
+
+    // 第2层:personalAgentId 和 personalIncomeAmount
+    if (record.personalAgentId && record.personalAgentId > 0 && record.personalIncomeAmount && Number(record.personalIncomeAmount) > 0) {
+      const orderPrice = Number(record.orderPrice) || 0
+      const personalIncomeAmount = Number(record.personalIncomeAmount)
+      // 计算分润比例
+      const rate = orderPrice > 0 ? (personalIncomeAmount / orderPrice) * 100 : 0
+
+      details.push({
+        level: 2,
+        agentId: record.personalAgentId,
+        rate: Number(rate.toFixed(2)),
+        amount: personalIncomeAmount
+      })
+    }
+
+    return details
+  }
+
+  /**
+   * 检查是否已存在索引记录
+   */
+  private async hasIndexRecords(incomeRecordId: number): Promise<boolean> {
+    const indexRepository = this.dataSource.getRepository(AgentCommissionIndex)
+    const count = await indexRepository.count({
+      where: { incomeRecordId }
+    })
+    return count > 0
+  }
+
+  /**
+   * 处理单条记录
+   */
+  private async processSingleRecord(record: IncomeRecords, skipExisting: boolean = true): Promise<void> {
+    try {
+      // 检查是否已存在索引记录
+      if (skipExisting) {
+        const exists = await this.hasIndexRecords(record.id)
+        if (exists) {
+          this.stats.skipped++
+          return
+        }
+      }
+
+      // 解析 commissionDetails
+      let commissionDetails: CommissionDetail[] | null = null
+      
+      if (record.commissionDetails) {
+        commissionDetails = this.parseCommissionDetails(record.commissionDetails)
+        if (commissionDetails) {
+          this.stats.withDetails++
+        }
+      }
+
+      // 如果没有 commissionDetails,根据字段生成
+      if (!commissionDetails || commissionDetails.length === 0) {
+        commissionDetails = this.generateCommissionDetailsFromFields(record)
+        this.stats.withoutDetails++
+      }
+
+      // 如果没有分润明细,跳过
+      if (!commissionDetails || commissionDetails.length === 0) {
+        this.stats.skipped++
+        return
+      }
+
+      // 创建索引记录
+      // 注意:createdAt 使用订单的创建时间(record.createdAt),而不是生成脚本时的时间
+      const indexRepository = this.dataSource.getRepository(AgentCommissionIndex)
+      const indexRecords = commissionDetails.map(detail =>
+        indexRepository.create({
+          incomeRecordId: record.id,
+          agentId: detail.agentId,
+          level: detail.level,
+          commissionRate: detail.rate,
+          commissionAmount: detail.amount,
+          orderNo: record.orderNo,
+          orderPrice: Number(record.orderPrice),
+          userId: record.userId,
+          status: record.status,
+          createdAt: record.createdAt // 使用订单的创建时间
+        })
+      )
+
+      await indexRepository.save(indexRecords)
+      this.stats.success++
+
+      if (this.stats.processed % 100 === 0) {
+        console.log(`已处理: ${this.stats.processed} | 成功: ${this.stats.success} | 失败: ${this.stats.failed} | 跳过: ${this.stats.skipped} | 有明细: ${this.stats.withDetails} | 无明细: ${this.stats.withoutDetails}`)
+      }
+    } catch (error) {
+      this.stats.failed++
+      console.error(`处理记录失败 (ID: ${record.id}, OrderNo: ${record.orderNo}):`, error)
+    }
+  }
+
+  /**
+   * 批量处理订单记录
+   */
+  async generateIndex(batchSize: number = 100, startId?: number, skipExisting: boolean = true): Promise<void> {
+    const incomeRecordsRepository = this.dataSource.getRepository(IncomeRecords)
+    const indexRepository = this.dataSource.getRepository(AgentCommissionIndex)
+
+    console.log('开始生成分润索引表记录...')
+    console.log(`批次大小: ${batchSize}`)
+    console.log(`起始ID: ${startId || '无'}`)
+    console.log(`跳过已存在: ${skipExisting}`)
+
+    // 先统计总数
+    let countQuery = incomeRecordsRepository
+      .createQueryBuilder('record')
+      .where('record.delFlag = :delFlag', { delFlag: false })
+
+    if (startId) {
+      countQuery = countQuery.andWhere('record.id >= :startId', { startId })
+    }
+
+    const total = await countQuery.getCount()
+    this.stats.total = total
+    console.log(`总记录数: ${total}`)
+
+    let offset = 0
+    let hasMore = true
+
+    while (hasMore) {
+      const queryBuilder = incomeRecordsRepository
+        .createQueryBuilder('record')
+        .where('record.delFlag = :delFlag', { delFlag: false })
+        .orderBy('record.id', 'ASC')
+        .limit(batchSize)
+        .offset(offset)
+
+      if (startId) {
+        queryBuilder.andWhere('record.id >= :startId', { startId })
+      }
+
+      const records = await queryBuilder.getMany()
+
+      if (records.length === 0) {
+        hasMore = false
+        break
+      }
+
+      for (const record of records) {
+        await this.processSingleRecord(record, skipExisting)
+        this.stats.processed++
+      }
+
+      offset += batchSize
+
+      // 如果这批数据少于批次大小,说明已经处理完所有数据
+      if (records.length < batchSize) {
+        hasMore = false
+      }
+    }
+
+    console.log('\n生成完成!')
+    console.log(`总计: ${this.stats.total}`)
+    console.log(`已处理: ${this.stats.processed}`)
+    console.log(`成功: ${this.stats.success}`)
+    console.log(`失败: ${this.stats.failed}`)
+    console.log(`跳过: ${this.stats.skipped}`)
+    console.log(`有明细: ${this.stats.withDetails}`)
+    console.log(`无明细: ${this.stats.withoutDetails}`)
+  }
+}
+
+// 主函数
+async function main() {
+  const app = await createApp()
+  const dataSource = (app as any).dataSource
+
+  if (!dataSource) {
+    console.error('无法获取数据库连接')
+    process.exit(1)
+  }
+
+  try {
+    const generator = new CommissionIndexGenerator(dataSource)
+
+    // 从命令行参数获取配置
+    const args = process.argv.slice(2)
+    const batchSize = parseInt(args[0]) || 100
+    const startId = args[1] ? parseInt(args[1]) : undefined
+    const skipExisting = args[2] !== 'false'
+
+    console.log('配置参数:')
+    console.log(`  批次大小: ${batchSize}`)
+    console.log(`  起始ID: ${startId || '无'}`)
+    console.log(`  跳过已存在: ${skipExisting}`)
+    console.log('')
+
+    await generator.generateIndex(batchSize, startId, skipExisting)
+  } catch (error) {
+    console.error('执行失败:', error)
+    process.exit(1)
+  } finally {
+    await app.close()
+  }
+}
+
+// 运行脚本
+if (require.main === module) {
+  main().catch(console.error)
+}
+
+export { CommissionIndexGenerator }
+

+ 4 - 0
src/app.ts

@@ -20,7 +20,9 @@ import paymentRoutes from './routes/payment.routes'
 import memberRoutes from './routes/member.routes'
 import userShareRoutes from './routes/user-invite.routes'
 import teamDomainRoutes from './routes/team-domain.routes'
+import landingDomainPoolRoutes from './routes/landing-domain-pool.routes'
 import bannerRoutes from './routes/banner.routes'
+import pageClickRecordRoutes from './routes/page-click-record.routes'
 import { authenticate } from './middlewares/auth.middleware'
 import { createRedisClient, closeRedisClient } from './config/redis'
 import { BannerStatisticsScheduler } from './scheduler/banner-statistics.scheduler'
@@ -96,11 +98,13 @@ export const createApp = async () => {
   app.register(teamRoutes, { prefix: '/api/teams' })
   app.register(teamMembersRoutes, { prefix: '/api/team-members' })
   app.register(teamDomainRoutes, { prefix: '/api/team-domains' })
+  app.register(landingDomainPoolRoutes, { prefix: '/api/landing-domain-pools' })
   app.register(promotionLinkRoutes, { prefix: '/api/links' })
   app.register(paymentRoutes, { prefix: '/api/payment' })
   app.register(memberRoutes, { prefix: '/api/member' })
   app.register(userShareRoutes, { prefix: '/api/user-share' })
   app.register(bannerRoutes, { prefix: '/api/banners' })
+  app.register(pageClickRecordRoutes, { prefix: '/api/page-clicks' })
 
   // 添加 /account 路由重定向到用户资料
   app.get('/account', { onRequest: [authenticate] }, async (request, reply) => {

+ 25 - 2
src/controllers/income-records.controller.ts

@@ -60,7 +60,23 @@ export class IncomeRecordsController {
     try {
       const { id } = request.params
       const incomeRecord = await this.incomeRecordsService.findById(id)
-      return reply.send(incomeRecord)
+      
+      // 解析分润信息(如果存在)
+      let commissionDetails = null
+      if (incomeRecord.commissionDetails) {
+        try {
+          commissionDetails = JSON.parse(incomeRecord.commissionDetails)
+        } catch (parseError) {
+          // 解析失败时返回 null
+          commissionDetails = null
+        }
+      }
+      
+      // 返回包含解析后的分润信息的记录
+      return reply.send({
+        ...incomeRecord,
+        commissionDetails
+      })
     } catch (error) {
       return reply.code(404).send({ message: '收入记录不存在' })
     }
@@ -73,16 +89,23 @@ export class IncomeRecordsController {
         return reply.code(403).send({ message: '用户未登录' })
       }
 
+      // 普通用户不能查看分销明细
+      if (user.role === UserRole.USER) {
+        return reply.code(403).send({ message: '普通用户无权查看分销明细' })
+      }
+
       // 根据用户角色设置查询条件
       if (user.role === UserRole.PROMOTER) {
-        // 推广用户只能查看自己相关的订单(通过personalAgentId过滤)
+        // 推广用户(团队成员)只能查看自己相关的订单(通过personalAgentId过滤)
         request.query.personalAgentId = user.id
       } else if (user.role === UserRole.TEAM) {
+        // 团队账号可以查看自己团队的订单
         const team = await this.teamService.findByUserId(user.id)
         if (team) {
           request.query.agentId = team.userId
         }
       }
+      // ADMIN 可以查看所有订单,不需要设置过滤条件
 
       const result = await this.incomeRecordsService.findAll(request.query)
       return reply.send(result)

+ 219 - 0
src/controllers/landing-domain-pool.controller.ts

@@ -0,0 +1,219 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { LandingDomainPoolService } from '../services/landing-domain-pool.service'
+import {
+  CreateLandingDomainPoolBody,
+  UpdateLandingDomainPoolBody,
+  ListLandingDomainPoolQuery,
+  LandingDomainPoolParams
+} from '../dto/landing-domain-pool.dto'
+import { UserRole } from '../entities/user.entity'
+import { TeamService } from '../services/team.service'
+
+export class LandingDomainPoolController {
+  private landingDomainPoolService: LandingDomainPoolService
+  private teamService: TeamService
+
+  constructor(app: FastifyInstance) {
+    this.landingDomainPoolService = new LandingDomainPoolService(app)
+    this.teamService = new TeamService(app)
+  }
+
+  async create(request: FastifyRequest<{ Body: CreateLandingDomainPoolBody }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      // 如果是TEAM用户,自动设置teamId并验证权限
+      if (user.role === UserRole.TEAM) {
+        const team = await this.teamService.findByUserId(user.id)
+        request.body.teamId = team.id
+      }
+
+      // 检查是否包含多个域名(通过中英文逗号、分号或换行分隔)
+      const hasMultipleDomains = /[,;\n\r]/.test(request.body.domain)
+
+      if (hasMultipleDomains) {
+        // 批量创建
+        const result = await this.landingDomainPoolService.createBatch(request.body)
+        return reply.code(201).send({
+          message: `批量创建完成,成功: ${result.success.length} 个,失败: ${result.failed.length} 个`,
+          success: result.success,
+          failed: result.failed
+        })
+      } else {
+        // 单个创建
+        const landingDomainPool = await this.landingDomainPoolService.create(request.body)
+        return reply.code(201).send(landingDomainPool)
+      }
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '创建落地域名池失败'
+      return reply.code(500).send({ message: errorMessage })
+    }
+  }
+
+  async findById(request: FastifyRequest<{ Params: LandingDomainPoolParams }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+      const landingDomainPool = await this.landingDomainPoolService.findById(id)
+      return reply.send(landingDomainPool)
+    } catch (error) {
+      return reply.code(404).send({ message: '落地域名池不存在' })
+    }
+  }
+
+  async findAll(request: FastifyRequest<{ Querystring: ListLandingDomainPoolQuery }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      if (user.role === UserRole.TEAM) {
+        const team = await this.teamService.findByUserId(user.id)
+        request.query.teamId = team.id
+      }
+
+      const result = await this.landingDomainPoolService.findAll(request.query)
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send({ message: '获取落地域名池列表失败' })
+    }
+  }
+
+  async showAll(request: FastifyRequest<{ Querystring: ListLandingDomainPoolQuery }>, reply: FastifyReply) {
+    try {
+      const result = await this.landingDomainPoolService.findAllGroupedByTeam(request.query)
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send({ message: '获取落地域名池列表失败' })
+    }
+  }
+
+  async update(request: FastifyRequest<{ Params: LandingDomainPoolParams; Body: UpdateLandingDomainPoolBody }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+      const updateData = { ...request.body, id }
+      const user = request.user
+
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      // 获取要更新的域名信息
+      const existingDomain = await this.landingDomainPoolService.findById(id)
+
+      // 如果是TEAM用户,只能更新自己团队的域名
+      if (user.role === UserRole.TEAM) {
+        const team = await this.teamService.findByUserId(user.id)
+        
+        // 验证域名属于该用户的团队
+        if (existingDomain.teamId !== team.id) {
+          return reply.code(403).send({ message: '无权限操作其他团队的域名' })
+        }
+
+        // TEAM用户不能修改teamId,保持原有的teamId
+        if (updateData.teamId !== undefined && updateData.teamId !== team.id) {
+          return reply.code(403).send({ message: '只能绑定到自己的团队' })
+        }
+        // 移除teamId字段,因为TEAM用户不能修改团队绑定
+        delete updateData.teamId
+      }
+
+      const updatedLandingDomainPool = await this.landingDomainPoolService.update(updateData)
+      return reply.send(updatedLandingDomainPool)
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '更新落地域名池失败'
+      return reply.code(500).send({ message: errorMessage })
+    }
+  }
+
+  async delete(request: FastifyRequest<{ Params: LandingDomainPoolParams }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+      const user = request.user
+
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      // 获取要删除的域名信息
+      const existingDomain = await this.landingDomainPoolService.findById(id)
+
+      // 如果是TEAM用户,只能删除自己团队的域名
+      if (user.role === UserRole.TEAM) {
+        const team = await this.teamService.findByUserId(user.id)
+        
+        // 验证域名属于该用户的团队
+        if (existingDomain.teamId !== team.id) {
+          return reply.code(403).send({ message: '无权限删除其他团队的域名' })
+        }
+      } else if (user.role !== UserRole.ADMIN) {
+        return reply.code(403).send({ message: '无权限' })
+      }
+
+      await this.landingDomainPoolService.delete(id)
+      return reply.send({ message: '落地域名池已删除' })
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '删除落地域名池失败'
+      return reply.code(500).send({ message: errorMessage })
+    }
+  }
+
+  async findByTeamId(request: FastifyRequest<{ Params: { teamId: number } }>, reply: FastifyReply) {
+    try {
+      const { teamId } = request.params
+      const user = request.user
+
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      if (user.role !== UserRole.ADMIN) {
+        return reply.code(403).send({ message: '无权限' })
+      }
+
+      const landingDomainPools = await this.landingDomainPoolService.findByTeamId(Number(teamId))
+      return reply.send(landingDomainPools)
+    } catch (error) {
+      return reply.code(500).send({ message: '获取落地域名池失败' })
+    }
+  }
+
+  async findByTeamDomain(request: FastifyRequest<{ Querystring: { domain: string } }>, reply: FastifyReply) {
+    try {
+      const { domain } = request.query
+
+      if (!domain) {
+        return reply.code(400).send({ message: '域名参数不能为空' })
+      }
+
+      const landingDomainPools = await this.landingDomainPoolService.findByTeamDomain(domain)
+      return reply.send(landingDomainPools)
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '获取落地域名池失败'
+      if (errorMessage === '域名不存在') {
+        return reply.code(404).send({ message: errorMessage })
+      }
+      return reply.code(500).send({ message: errorMessage })
+    }
+  }
+
+  async getRetentionDomainsByLandingDomain(request: FastifyRequest<{ Querystring: { domain: string } }>, reply: FastifyReply) {
+    try {
+      const { domain } = request.query
+
+      if (!domain) {
+        return reply.code(400).send({ message: '域名参数不能为空' })
+      }
+
+      const retentionDomains = await this.landingDomainPoolService.getRetentionDomainsByLandingDomain(domain)
+      return reply.send(retentionDomains)
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '获取留存域名失败'
+      return reply.code(500).send({ message: errorMessage })
+    }
+  }
+}
+

+ 6 - 5
src/controllers/member.controller.ts

@@ -23,9 +23,9 @@ export class MemberController {
     this.userService = new UserService(app)
   }
 
-  async createGuest(request: FastifyRequest<{ Querystring: { code?: string; ref?: string } }>, reply: FastifyReply) {
+  async createGuest(request: FastifyRequest<{ Querystring: { code?: string; ref?: string; landingDomain?: string } }>, reply: FastifyReply) {
     try {
-      const { code, ref } = request.query || {}
+      const { code, ref, landingDomain } = request.query || {}
 
       const ip =
         request.ip ||
@@ -40,7 +40,7 @@ export class MemberController {
         domain = request.headers.origin
       }
 
-      const user = await this.memberService.createGuest(code, domain, ip)
+      const user = await this.memberService.createGuest(code, domain, ip, landingDomain)
       const token = await reply.jwtSign({ id: user.id, name: user.name, role: user.role })
 
       return reply.code(201).send({
@@ -93,7 +93,7 @@ export class MemberController {
 
   async register(request: FastifyRequest<{ Body: RegisterBody }>, reply: FastifyReply) {
     try {
-      const { name, password, email, phone, code, memberCode } = request.body
+      const { name, password, email, phone, code, memberCode, landingDomain } = request.body
 
       // 验证必填字段
       if (!name || !password) {
@@ -128,7 +128,8 @@ export class MemberController {
         'unknown'
 
       // 调用注册服务
-      const { user, member } = await this.memberService.register(name, password, email, phone, code, ip, memberCode)
+      // 校验顺序:推广码 -> 落地域名 -> IP历史记录
+      const { user, member } = await this.memberService.register(name, password, email, phone, code, ip, memberCode, landingDomain)
 
       // 生成JWT token
       const token = await reply.jwtSign({ id: user.id, name: user.name, role: user.role })

+ 160 - 0
src/controllers/page-click-record.controller.ts

@@ -0,0 +1,160 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { PageClickRecordService } from '../services/page-click-record.service'
+import {
+  RecordPageClickBody,
+  GetPageClickStatisticsQuery,
+  PageClickStatisticsResponse,
+  PageType
+} from '../dto/page-click-record.dto'
+
+export class PageClickRecordController {
+  private pageClickRecordService: PageClickRecordService
+
+  constructor(app: FastifyInstance) {
+    this.pageClickRecordService = new PageClickRecordService(app)
+  }
+
+  /**
+   * 记录页面点击
+   * 公开接口,无需认证
+   */
+  async recordClick(
+    request: FastifyRequest<{ Body: RecordPageClickBody }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { pageType } = request.body
+
+      if (!pageType || !Object.values(PageType).includes(pageType)) {
+        return reply.code(400).send({ 
+          message: 'pageType 为必填字段,必须是 "home" 或 "video"' 
+        })
+      }
+
+      // 获取客户端IP
+      const ip = request.ip || request.headers['x-forwarded-for'] || request.socket.remoteAddress
+      const clientIp = Array.isArray(ip) ? ip[0] : ip || ''
+
+      const clickCount = await this.pageClickRecordService.recordClick(pageType, clientIp)
+
+      return reply.send({
+        success: true,
+        pageType,
+        clickCount,
+        message: '点击记录成功'
+      })
+    } catch (error) {
+      if (error instanceof Error && error.message === 'Redis未配置') {
+        return reply.code(503).send({ message: 'Redis服务未配置,无法记录点击' })
+      }
+      if (error instanceof Error && error.message.includes('不支持的页面类型')) {
+        return reply.code(400).send({ message: error.message })
+      }
+      return reply.code(500).send({ message: '记录点击失败' })
+    }
+  }
+
+  /**
+   * 获取页面点击统计
+   * 管理后台接口,需要管理员权限
+   */
+  async getStatistics(
+    request: FastifyRequest<{ Querystring: GetPageClickStatisticsQuery }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { pageType, startDate, endDate } = request.query
+
+      // 验证页面类型(如果提供)
+      if (pageType && !Object.values(PageType).includes(pageType)) {
+        return reply.code(400).send({ 
+          message: 'pageType 必须是 "home" 或 "video"' 
+        })
+      }
+
+      // 默认查询最近7天的数据
+      const end = endDate ? new Date(endDate) : new Date()
+      const start = startDate
+        ? new Date(startDate)
+        : new Date(end.getTime() - 6 * 24 * 60 * 60 * 1000) // 7天前
+
+      // 验证日期格式
+      if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+        return reply.code(400).send({ message: '日期格式错误,请使用 YYYY-MM-DD 格式' })
+      }
+
+      if (start > end) {
+        return reply.code(400).send({ message: '开始日期不能晚于结束日期' })
+      }
+
+      const result = await this.pageClickRecordService.getStatistics(start, end, pageType)
+
+      return reply.send({
+        success: true,
+        ...result,
+        query: {
+          pageType: pageType || 'all',
+          startDate: start.toISOString().split('T')[0],
+          endDate: end.toISOString().split('T')[0]
+        }
+      } as PageClickStatisticsResponse & { success: boolean; query: any })
+    } catch (error) {
+      return reply.code(500).send({ message: '获取统计数据失败' })
+    }
+  }
+
+  /**
+   * 获取今日点击统计汇总
+   * 管理后台接口,需要管理员权限
+   */
+  async getTodaySummary(request: FastifyRequest, reply: FastifyReply) {
+    try {
+      const summary = await this.pageClickRecordService.getTodaySummary()
+
+      return reply.send({
+        success: true,
+        date: new Date().toISOString().split('T')[0],
+        summary
+      })
+    } catch (error) {
+      return reply.code(500).send({ message: '获取今日统计失败' })
+    }
+  }
+
+  /**
+   * 获取指定页面的点击量
+   * 管理后台接口,需要管理员权限
+   */
+  async getPageClickCount(
+    request: FastifyRequest<{ Querystring: { pageType: PageType; date?: string } }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { pageType, date } = request.query
+
+      if (!pageType || !Object.values(PageType).includes(pageType)) {
+        return reply.code(400).send({ 
+          message: 'pageType 为必填字段,必须是 "home" 或 "video"' 
+        })
+      }
+
+      const targetDate = date ? new Date(date) : new Date()
+
+      if (isNaN(targetDate.getTime())) {
+        return reply.code(400).send({ message: '日期格式错误,请使用 YYYY-MM-DD 格式' })
+      }
+
+      const clickCount = await this.pageClickRecordService.getClickCount(pageType, targetDate)
+
+      return reply.send({
+        success: true,
+        pageType,
+        date: targetDate.toISOString().split('T')[0],
+        clickCount
+      })
+    } catch (error) {
+      return reply.code(500).send({ message: '获取点击量失败' })
+    }
+  }
+}
+

+ 27 - 60
src/controllers/team-domain.controller.ts

@@ -12,6 +12,7 @@ import { TeamService } from '../services/team.service'
 import { Repository, Between } from 'typeorm'
 import { Member } from '../entities/member.entity'
 import { IncomeRecords } from '../entities/income-records.entity'
+import { MultiLevelCommissionService } from '../services/multi-level-commission.service'
 
 export class TeamDomainController {
   private teamDomainService: TeamDomainService
@@ -19,6 +20,7 @@ export class TeamDomainController {
   private teamService: TeamService
   private memberRepository: Repository<Member>
   private incomeRecordsRepository: Repository<IncomeRecords>
+  private multiLevelCommissionService: MultiLevelCommissionService
 
   constructor(app: FastifyInstance) {
     this.teamDomainService = new TeamDomainService(app)
@@ -26,6 +28,7 @@ export class TeamDomainController {
     this.teamService = new TeamService(app)
     this.memberRepository = app.dataSource.getRepository(Member)
     this.incomeRecordsRepository = app.dataSource.getRepository(IncomeRecords)
+    this.multiLevelCommissionService = new MultiLevelCommissionService(app)
   }
 
   async create(request: FastifyRequest<{ Body: CreateTeamDomainBody }>, reply: FastifyReply) {
@@ -254,8 +257,8 @@ export class TeamDomainController {
           const teamDomains = await this.teamDomainService.findByTeamMemberId(teamMembers.id)
           const teamDomainIds = teamDomains.map(d => d.id)
           
-          // 获取推广用户相关的域名统计(只统计personalIncomeAmount
-          const result = await this.getPromoterDailyStatistics(teamDomainIds, domain)
+          // 获取推广用户相关的域名统计(使用索引表,统计该推广员的所有分润
+          const result = await this.getPromoterDailyStatistics(teamDomainIds, user.id, domain)
           return reply.send(result)
         } catch (error) {
           // 如果推广用户没有团队成员记录,返回空统计
@@ -301,8 +304,8 @@ export class TeamDomainController {
           const teamDomains = await this.teamDomainService.findByTeamMemberId(teamMembers.id)
           const teamDomainIds = teamDomains.map(d => d.id)
           
-          // 获取推广用户相关的域名统计(只统计personalIncomeAmount
-          const result = await this.getPromoterAllStatistics(teamDomainIds, domain)
+          // 获取推广用户相关的域名统计(使用索引表,统计该推广员的所有分润
+          const result = await this.getPromoterAllStatistics(teamDomainIds, user.id, domain)
           return reply.send(result)
         } catch (error) {
           // 如果推广用户没有团队成员记录,返回空统计
@@ -333,9 +336,9 @@ export class TeamDomainController {
   }
 
   /**
-   * 获取推广用户每日统计(只统计personalIncomeAmount
+   * 获取推广用户每日统计(使用索引表,统计该推广员的所有分润
    */
-  private async getPromoterDailyStatistics(domainIds: number[], domain?: string): Promise<any[]> {
+  private async getPromoterDailyStatistics(domainIds: number[], promoterUserId: number, domain?: string): Promise<any[]> {
     if (domainIds.length === 0) {
       return []
     }
@@ -373,33 +376,15 @@ export class TeamDomainController {
         }
       })
 
-      // 统计今日收入(只统计personalIncomeAmount)
-      const todayIncomeRecords = await this.incomeRecordsRepository
-        .createQueryBuilder('record')
-        .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-        .where('m.domainId = :domainId', { domainId: domainId })
-        .andWhere('record.createdAt >= :startDate', { startDate: today })
-        .andWhere('record.createdAt < :endDate', { endDate: tomorrow })
-        .andWhere('record.delFlag = :delFlag', { delFlag: false })
-        .andWhere('record.status = :status', { status: true })
-        .andWhere('record.personalIncomeAmount > 0')
-        .getRawOne()
-
-      const todayIncome = todayIncomeRecords?.totalIncome ? parseFloat(todayIncomeRecords.totalIncome) : 0
-
-      // 统计历史总收入(只统计personalIncomeAmount)
-      const totalIncomeRecords = await this.incomeRecordsRepository
-        .createQueryBuilder('record')
-        .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-        .where('m.domainId = :domainId', { domainId: domainId })
-        .andWhere('record.delFlag = :delFlag', { delFlag: false })
-        .andWhere('record.status = :status', { status: true })
-        .andWhere('record.personalIncomeAmount > 0')
-        .getRawOne()
+      // 统计今日收入(使用索引表,统计该推广员的所有分润)
+      const todayIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(
+        promoterUserId,
+        today,
+        tomorrow
+      )
 
-      const totalIncome = totalIncomeRecords?.totalIncome ? parseFloat(totalIncomeRecords.totalIncome) : 0
+      // 统计历史总收入(使用索引表,统计该推广员的所有分润)
+      const totalIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(promoterUserId)
 
       // 统计历史总销售额
       const totalSalesRecords = await this.incomeRecordsRepository
@@ -443,9 +428,9 @@ export class TeamDomainController {
   }
 
   /**
-   * 获取推广用户总统计(只统计personalIncomeAmount
+   * 获取推广用户总统计(使用索引表,统计该推广员的所有分润
    */
-  private async getPromoterAllStatistics(domainIds: number[], domain?: string): Promise<any[]> {
+  private async getPromoterAllStatistics(domainIds: number[], promoterUserId: number, domain?: string): Promise<any[]> {
     if (domainIds.length === 0) {
       return []
     }
@@ -482,33 +467,15 @@ export class TeamDomainController {
         }
       })
 
-      // 统计总收入(只统计personalIncomeAmount)
-      const totalIncomeRecords = await this.incomeRecordsRepository
-        .createQueryBuilder('record')
-        .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-        .where('m.domainId = :domainId', { domainId: domainId })
-        .andWhere('record.delFlag = :delFlag', { delFlag: false })
-        .andWhere('record.status = :status', { status: true })
-        .andWhere('record.personalIncomeAmount > 0')
-        .getRawOne()
-
-      const totalIncome = totalIncomeRecords?.totalIncome ? parseFloat(totalIncomeRecords.totalIncome) : 0
-
-      // 统计今日收入(只统计personalIncomeAmount)
-      const todayIncomeRecords = await this.incomeRecordsRepository
-        .createQueryBuilder('record')
-        .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-        .where('m.domainId = :domainId', { domainId: domainId })
-        .andWhere('record.createdAt >= :startDate', { startDate: today })
-        .andWhere('record.createdAt < :endDate', { endDate: tomorrow })
-        .andWhere('record.delFlag = :delFlag', { delFlag: false })
-        .andWhere('record.status = :status', { status: true })
-        .andWhere('record.personalIncomeAmount > 0')
-        .getRawOne()
+      // 统计总收入(使用索引表,统计该推广员的所有分润)
+      const totalIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(promoterUserId)
 
-      const todayIncome = todayIncomeRecords?.totalIncome ? parseFloat(todayIncomeRecords.totalIncome) : 0
+      // 统计今日收入(使用索引表,统计该推广员的所有分润)
+      const todayIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(
+        promoterUserId,
+        today,
+        tomorrow
+      )
 
       // 统计历史总销售额
       const totalSalesRecords = await this.incomeRecordsRepository

+ 6 - 1
src/controllers/user.controller.ts

@@ -126,7 +126,12 @@ export class UserController {
 
       return reply.send(result)
     } catch (error) {
-      return reply.code(500).send({ message: '获取用户列表失败' })
+      request.log.error({ error }, '获取用户列表失败')
+      const errorMessage = error instanceof Error ? error.message : '获取用户列表失败'
+      return reply.code(500).send({ 
+        message: '获取用户列表失败',
+        error: errorMessage
+      })
     }
   }
 

+ 33 - 0
src/dto/landing-domain-pool.dto.ts

@@ -0,0 +1,33 @@
+import { FastifyRequest } from 'fastify'
+import { Pagination } from './common.dto'
+import { DomainType } from '../entities/landing-domain-pool.entity'
+
+export interface CreateLandingDomainPoolBody {
+  teamId: number
+  domain: string // 支持单个域名或批量域名(用逗号或换行分隔)
+  description?: string
+  userId?: number // 绑定的团队成员ID
+  domainType?: DomainType // 域名类型:落地域名或留存域名
+}
+
+export interface UpdateLandingDomainPoolBody {
+  id: number
+  teamId?: number
+  domain?: string
+  description?: string
+  userId?: number // 绑定的团队成员ID
+  domainType?: DomainType // 域名类型:落地域名或留存域名
+}
+
+export interface ListLandingDomainPoolQuery extends Pagination {
+  id?: number
+  teamId?: number
+  domain?: string
+  userId?: number // 按团队成员ID过滤
+  domainType?: DomainType // 按域名类型过滤
+}
+
+export interface LandingDomainPoolParams {
+  id: number
+}
+

+ 1 - 0
src/dto/member.dto.ts

@@ -67,6 +67,7 @@ export interface RegisterBody {
   phone?: string
   code?: string
   memberCode?: string
+  landingDomain?: string // 落地域名,通过落地域名查询相关团队进行绑定
 }
 
 export interface UpdateProfileBody {

+ 30 - 0
src/dto/page-click-record.dto.ts

@@ -0,0 +1,30 @@
+// 页面类型枚举
+export enum PageType {
+  HOME = 'home', // 首页
+  VIDEO = 'video' // 视频页面
+}
+
+export interface RecordPageClickBody {
+  pageType: PageType // 页面类型:home 或 video
+}
+
+export interface GetPageClickStatisticsQuery {
+  pageType?: PageType // 可选,指定页面类型,不传则返回所有页面
+  startDate?: string // 可选,开始日期 YYYY-MM-DD,默认7天前
+  endDate?: string // 可选,结束日期 YYYY-MM-DD,默认今天
+}
+
+export interface PageClickStatistics {
+  pageType: PageType
+  date: string // YYYY-MM-DD
+  clickCount: number
+}
+
+export interface PageClickStatisticsResponse {
+  statistics: PageClickStatistics[]
+  total: {
+    pageType: PageType
+    totalClicks: number
+  }[]
+}
+

+ 59 - 15
src/dto/team-members.dto.ts

@@ -5,6 +5,7 @@ export interface CreateTeamMembersBody {
   name: string
   teamId: number
   teamUserId?: number
+  memberId?: number // 会员ID,如果提供则同步关联该会员
   password?: string
   totalRevenue?: number
   todayRevenue?: number
@@ -16,6 +17,7 @@ export interface UpdateTeamMembersBody {
   name?: string
   teamId?: number
   userId?: number
+  memberId?: number // 父级团队成员的 id(team_members 表的 id),用于更新 parentId
   totalRevenue?: number
   todayRevenue?: number
   commissionRate?: number // 分成比例
@@ -70,21 +72,29 @@ export interface TeamLeaderStatsResponse {
   teamId: number
   teamName: string
   teamCommissionRate: number
-  membersStats: Array<{
-    memberId: number
-    memberName: string
-    personalCommissionRate: number
-    actualRate: number
-    // 个人收入统计
-    totalRevenue: number
-    todayRevenue: number
-    // 个人销售额统计
-    totalSales: number
-    todaySales: number
-    // 队长实际收入统计(所有成员都显示)
-    teamLeaderTotalIncome: number
-    teamLeaderTodayIncome: number
-  }>
+  membersStats: TeamMemberStatsTreeNode[] // 改为树状结构
+}
+
+export interface TeamMemberStatsTreeNode {
+  id: number
+  name: string
+  userId: number
+  teamId: number
+  commissionRate: number
+  parentId: number | null
+  // 统计信息
+  totalRevenue: number
+  todayRevenue: number
+  totalSales: number
+  todaySales: number
+  todayDAU: number
+  todayNewUsers: number
+  // 队长实际收入统计(仅 teamMember 类型有)
+  teamLeaderTotalIncome?: number
+  teamLeaderTodayIncome?: number
+  // 树状结构
+  type: 'team' | 'teamMember' | 'other'
+  children: TeamMemberStatsTreeNode[]
 }
 
 export interface PromotionLinkResponse {
@@ -97,4 +107,38 @@ export interface GeneratePromoCodeResponse {
   teamMemberId: number
   promoCode: string
   message: string
+}
+
+export interface TeamMemberTreeNode {
+  id: number
+  name: string
+  userId: number
+  teamId: number
+  commissionRate: number
+  totalRevenue: number
+  todayRevenue: number
+  promoCode: string | null
+  createdAt: Date
+  updatedAt: Date
+  type: 'team' | 'teamMember' | 'other'
+  children: TeamMemberTreeNode[]
+}
+
+export interface TeamMemberStatsTreeNode {
+  id: number
+  name: string
+  userId: number
+  teamId: number
+  commissionRate: number
+  parentId: number | null
+  // 统计信息
+  totalRevenue: number
+  todayRevenue: number
+  totalSales: number
+  todaySales: number
+  todayDAU: number
+  todayNewUsers: number
+  // 树状结构
+  type: 'team' | 'teamMember' | 'other'
+  children: TeamMemberStatsTreeNode[]
 }

+ 49 - 0
src/entities/agent-commission-index.entity.ts

@@ -0,0 +1,49 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Index, ManyToOne, JoinColumn } from 'typeorm'
+import { IncomeRecords } from './income-records.entity'
+
+@Entity('agent_commission_index')
+@Index('idx_agent_id', ['agentId'])
+@Index('idx_income_record_id', ['incomeRecordId'])
+@Index('idx_agent_status_created', ['agentId', 'status', 'createdAt'])
+@Index('idx_order_no', ['orderNo'])
+@Index('idx_level', ['level'])
+@Index('idx_user_id', ['userId'])
+export class AgentCommissionIndex {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column()
+  incomeRecordId: number
+
+  @ManyToOne(() => IncomeRecords, { onDelete: 'CASCADE' })
+  @JoinColumn({ name: 'incomeRecordId' })
+  incomeRecord: IncomeRecords
+
+  @Column()
+  agentId: number
+
+  @Column()
+  level: number
+
+  @Column({ type: 'decimal', precision: 5, scale: 2 })
+  commissionRate: number
+
+  @Column({ type: 'decimal', precision: 10, scale: 5 })
+  commissionAmount: number
+
+  @Column({ length: 50 })
+  orderNo: string
+
+  @Column({ type: 'decimal', precision: 10, scale: 5 })
+  orderPrice: number
+
+  @Column()
+  userId: number
+
+  @Column({ default: false })
+  status: boolean
+
+  @CreateDateColumn()
+  createdAt: Date
+}
+

+ 2 - 2
src/entities/banner-daily-statistics.entity.ts

@@ -7,13 +7,13 @@ export class BannerDailyStatistics {
   @PrimaryGeneratedColumn()
   id: number
 
-  @Column()
+  @Column({ type: 'int' })
   bannerId: number
 
   @Column({ type: 'date' })
   statDate: Date
 
-  @Column({ default: 0 })
+  @Column({ type: 'int', default: 0 })
   uniqueIpCount: number
 
   @CreateDateColumn()

+ 2 - 2
src/entities/banner.entity.ts

@@ -23,10 +23,10 @@ export class Banner {
   @Column({ length: 500 })
   link: string
 
-  @Column({ default: 0 })
+  @Column({ type: 'int', default: 0 })
   clickCount: number
 
-  @Column({ default: 0 })
+  @Column({ type: 'int', default: 0 })
   todayClickCount: number
 
   @Column({

+ 2 - 2
src/entities/finance.entity.ts

@@ -11,10 +11,10 @@ export class Finance {
   @PrimaryGeneratedColumn()
   id: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   teamId: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   userId: number
 
   @Column({

+ 6 - 3
src/entities/income-records.entity.ts

@@ -25,13 +25,13 @@ export class IncomeRecords {
   @PrimaryGeneratedColumn()
   id: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   agentId: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   userId: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   personalAgentId: number
 
   @Column({ type: 'decimal', precision: 10, scale: 5, default: 0 })
@@ -83,6 +83,9 @@ export class IncomeRecords {
   @Column({ default: false })
   delFlag: boolean
 
+  @Column({ type: 'text', nullable: true })
+  commissionDetails: string // JSON格式存储分润明细:CommissionDetail[]
+
   @CreateDateColumn()
   createdAt: Date
 

+ 38 - 0
src/entities/landing-domain-pool.entity.ts

@@ -0,0 +1,38 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+
+export enum DomainType {
+  LANDING = 'landing', // 落地域名
+  RETENTION = 'retention' // 留存域名
+}
+
+@Entity()
+export class LandingDomainPool {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column()
+  teamId: number
+
+  @Column({ length: 100 })
+  domain: string
+
+  @Column({ length: 500, nullable: true })
+  description: string
+
+  @Column({ type: 'int', nullable: true })
+  userId: number
+
+  @Column({
+    type: 'enum',
+    enum: DomainType,
+    default: DomainType.LANDING
+  })
+  domainType: DomainType
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+}
+

+ 3 - 3
src/entities/member.entity.ts

@@ -28,13 +28,13 @@ export class Member {
   @PrimaryGeneratedColumn()
   id: number
 
-  @Column()
+  @Column({ type: 'int' })
   userId: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   teamId: number
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   domainId: number
 
   @Column({ unique: true, length: 100, nullable: true })

+ 1 - 1
src/entities/promotion-link.entity.ts

@@ -14,7 +14,7 @@ export class PromotionLink {
   @Column()
   teamId: number
 
-  @Column({ nullable: true })
+  @Column({ type: 'int', nullable: true })
   memberId: number
 
   @Column()

+ 1 - 1
src/entities/sys-config.entity.ts

@@ -16,7 +16,7 @@ export class SysConfig {
   @PrimaryGeneratedColumn()
   id: number
 
-  @Column({ default: 0 })
+  @Column({ type: 'int', default: 0 })
   teamId: number
 
   @Column({ length: 100 })

+ 1 - 1
src/entities/team-domain.entity.ts

@@ -8,7 +8,7 @@ export class TeamDomain {
   @Column()
   teamId: number
 
-  @Column({ nullable: true })
+  @Column({ type: 'int', nullable: true })
   teamMemberId: number
 
   @Column({ length: 100 })

+ 3 - 0
src/entities/team-members.entity.ts

@@ -26,6 +26,9 @@ export class TeamMembers {
   @Column({ unique: true, length: 10, nullable: true })
   promoCode: string
 
+  @Column({ type: 'int', nullable: true })
+  parentId: number | null // 父级团队成员的 id(team_members 表的 id)
+
   @CreateDateColumn()
   createdAt: Date
 

+ 2 - 2
src/entities/user-invite-record.entity.ts

@@ -11,7 +11,7 @@ export class UserShareRecord {
   @Column()
   inviterId: number
 
-  @Column({ nullable: true })
+  @Column({ type: 'int', nullable: true })
   invitedUserId: number
 
   @Column({ length: 100 })
@@ -26,7 +26,7 @@ export class UserShareRecord {
   @Column({ length: 100, nullable: true })
   inviterName: string
 
-  @Column({ nullable: true })
+  @Column({ type: 'int', nullable: true })
   teamId: number
 
   @Column({ length: 100, nullable: true })

+ 3 - 2
src/entities/user.entity.ts

@@ -1,4 +1,4 @@
-import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'
 
 export enum UserRole {
   ADMIN = 'admin',
@@ -8,6 +8,7 @@ export enum UserRole {
 }
 
 @Entity()
+@Index('idx_user_parent_id', ['parentId'])
 export class User {
   @PrimaryGeneratedColumn()
   id: number
@@ -31,6 +32,6 @@ export class User {
   })
   role: UserRole
 
-  @Column({ nullable: true, default: 0 })
+  @Column({ type: 'int', nullable: true, default: 0 })
   parentId: number
 }

+ 76 - 0
src/routes/landing-domain-pool.routes.ts

@@ -0,0 +1,76 @@
+import { FastifyInstance } from 'fastify'
+import { LandingDomainPoolController } from '../controllers/landing-domain-pool.controller'
+import { authenticate, hasAnyRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+import {
+  CreateLandingDomainPoolBody,
+  UpdateLandingDomainPoolBody,
+  ListLandingDomainPoolQuery,
+  LandingDomainPoolParams
+} from '../dto/landing-domain-pool.dto'
+
+export default async function landingDomainPoolRoutes(fastify: FastifyInstance) {
+  const landingDomainPoolController = new LandingDomainPoolController(fastify)
+
+  // 创建落地域名池
+  fastify.post<{ Body: CreateLandingDomainPoolBody }>(
+    '/',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    landingDomainPoolController.create.bind(landingDomainPoolController)
+  )
+
+  // 获取落地域名池列表
+  fastify.get<{ Querystring: ListLandingDomainPoolQuery }>(
+    '/',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    landingDomainPoolController.findAll.bind(landingDomainPoolController)
+  )
+
+  // 管理员获取所有落地域名池列表
+  fastify.get<{ Querystring: ListLandingDomainPoolQuery }>(
+    '/show',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN)] },
+    landingDomainPoolController.showAll.bind(landingDomainPoolController)
+  )
+
+  // 获取单个落地域名池
+  fastify.get<{ Params: LandingDomainPoolParams }>(
+    '/:id',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    landingDomainPoolController.findById.bind(landingDomainPoolController)
+  )
+
+  // 更新落地域名池
+  fastify.put<{ Params: LandingDomainPoolParams; Body: UpdateLandingDomainPoolBody }>(
+    '/:id',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    landingDomainPoolController.update.bind(landingDomainPoolController)
+  )
+
+  // 删除落地域名池
+  fastify.delete<{ Params: LandingDomainPoolParams }>(
+    '/:id',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    landingDomainPoolController.delete.bind(landingDomainPoolController)
+  )
+
+  // 根据团队ID获取落地域名池列表
+  fastify.get<{ Params: { teamId: number } }>(
+    '/team/:teamId',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN)] },
+    landingDomainPoolController.findByTeamId.bind(landingDomainPoolController)
+  )
+
+  // 根据域名(team-domain)获取该域名所属团队的落地域名池列表(无需认证)
+  fastify.get<{ Querystring: { domain: string } }>(
+    '/by-domain',
+    landingDomainPoolController.findByTeamDomain.bind(landingDomainPoolController)
+  )
+
+  // 根据当前落地域名获取留存域名(无需认证)
+  fastify.get<{ Querystring: { domain: string } }>(
+    '/retention-by-landing',
+    landingDomainPoolController.getRetentionDomainsByLandingDomain.bind(landingDomainPoolController)
+  )
+}
+

+ 1 - 1
src/routes/member.routes.ts

@@ -18,7 +18,7 @@ export default async function memberRoutes(fastify: FastifyInstance) {
   const memberController = new MemberController(fastify)
 
   // 创建游客
-  fastify.get<{ Querystring: { code?: string; ref?: string } }>(
+  fastify.get<{ Querystring: { code?: string; ref?: string; landingDomain?: string } }>(
     '/guest',
     memberController.createGuest.bind(memberController)
   )

+ 42 - 0
src/routes/page-click-record.routes.ts

@@ -0,0 +1,42 @@
+import { FastifyInstance } from 'fastify'
+import { PageClickRecordController } from '../controllers/page-click-record.controller'
+import { authenticate, hasRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+import {
+  RecordPageClickBody,
+  GetPageClickStatisticsQuery,
+  PageType
+} from '../dto/page-click-record.dto'
+
+export default async function pageClickRecordRoutes(fastify: FastifyInstance) {
+  const pageClickRecordController = new PageClickRecordController(fastify)
+
+  // 记录页面点击(公开接口,无需认证)
+  fastify.post<{ Body: RecordPageClickBody }>(
+    '/click',
+    { onRequest: [] },
+    pageClickRecordController.recordClick.bind(pageClickRecordController)
+  )
+
+  // 获取页面点击统计(管理后台,需要管理员权限)
+  fastify.get<{ Querystring: GetPageClickStatisticsQuery }>(
+    '/statistics',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    pageClickRecordController.getStatistics.bind(pageClickRecordController)
+  )
+
+  // 获取今日点击统计汇总(管理后台,需要管理员权限)
+  fastify.get(
+    '/statistics/today',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    pageClickRecordController.getTodaySummary.bind(pageClickRecordController)
+  )
+
+  // 获取指定页面的点击量(管理后台,需要管理员权限)
+  fastify.get<{ Querystring: { pageType: PageType; date?: string } }>(
+    '/count',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    pageClickRecordController.getPageClickCount.bind(pageClickRecordController)
+  )
+}
+

+ 90 - 26
src/services/income-records.service.ts

@@ -2,20 +2,26 @@ import { Repository, Between, Like } from 'typeorm'
 import { FastifyInstance } from 'fastify'
 import { IncomeRecords, IncomeType, OrderType } from '../entities/income-records.entity'
 import { Member } from '../entities/member.entity'
+import { Team } from '../entities/team.entity'
 import { PaginationResponse } from '../dto/common.dto'
 import { CreateIncomeRecordBody, UpdateIncomeRecordBody, ListIncomeRecordsQuery } from '../dto/income-records.dto'
+import { MultiLevelCommissionService } from './multi-level-commission.service'
 
 export class IncomeRecordsService {
   private incomeRecordsRepository: Repository<IncomeRecords>
   private memberRepository: Repository<Member>
+  private teamRepository: Repository<Team>
+  private multiLevelCommissionService: MultiLevelCommissionService
 
   constructor(app: FastifyInstance) {
     this.incomeRecordsRepository = app.dataSource.getRepository(IncomeRecords)
     this.memberRepository = app.dataSource.getRepository(Member)
+    this.teamRepository = app.dataSource.getRepository(Team)
+    this.multiLevelCommissionService = new MultiLevelCommissionService(app)
   }
 
-  async create(data: CreateIncomeRecordBody): Promise<IncomeRecords> {
-    const incomeRecord = this.incomeRecordsRepository.create({
+  async create(data: CreateIncomeRecordBody & { commissionDetails?: string | null }): Promise<IncomeRecords> {
+    const recordData: any = {
       agentId: data.agentId,
       userId: data.userId,
       personalAgentId: data.personalAgentId || 0,
@@ -28,13 +34,21 @@ export class IncomeRecordsService {
       payChannel: data.payChannel,
       payNo: data.payNo,
       resourceId: data.resourceId,
-      status: data.status ?? false  // 修复:默认值应该是false,等待支付成功后才更新为true
-    })
-    return this.incomeRecordsRepository.save(incomeRecord)
+      status: data.status ?? false
+    }
+    
+    if (data.commissionDetails !== undefined && data.commissionDetails !== null) {
+      recordData.commissionDetails = data.commissionDetails
+    }
+    
+    const incomeRecord = this.incomeRecordsRepository.create(recordData)
+    const saved = await this.incomeRecordsRepository.save(incomeRecord)
+    return Array.isArray(saved) ? saved[0] : saved
   }
 
   async findById(id: number): Promise<IncomeRecords> {
-    return this.incomeRecordsRepository.findOneOrFail({ where: { id } })
+    const record = await this.incomeRecordsRepository.findOneOrFail({ where: { id } })
+    return record
   }
 
   async findAll(query: ListIncomeRecordsQuery): Promise<PaginationResponse<IncomeRecords>> {
@@ -114,6 +128,28 @@ export class IncomeRecordsService {
     // 如果有指定字段,则只查询这些字段
     if (select && select.length > 0) {
       queryOptions.select = select
+    } else {
+      // 列表接口默认排除 commissionDetails 字段
+      queryOptions.select = [
+        'id',
+        'agentId',
+        'userId',
+        'personalAgentId',
+        'personalIncomeAmount',
+        'incomeAmount',
+        'incomeType',
+        'orderType',
+        'orderPrice',
+        'orderNo',
+        'payChannel',
+        'payNo',
+        'resourceId',
+        'status',
+        'delFlag',
+        'createdAt',
+        'updatedAt'
+        // 不包含 commissionDetails
+      ]
     }
 
     const [records, total] = await this.incomeRecordsRepository.findAndCount(queryOptions)
@@ -226,30 +262,27 @@ export class IncomeRecordsService {
       currentDate.setDate(currentDate.getDate() + 1)
     }
 
-    // 构建基础查询条件
+    // 使用 incomeAmount 统计分润(团队和全部统计使用 incomeAmount)
+    // 构建基础查询条件(使用 income_records 表)
     let queryBuilder = this.incomeRecordsRepository
       .createQueryBuilder('record')
       .select([
         'record.agentId as agentId',
         'DATE_FORMAT(record.createdAt, "%Y-%m-%d") as date',
-        'SUM(IF(record.incomeType = :tipType, record.incomeAmount, 0)) as tipAmount',
-        'SUM(IF(record.incomeType = :commissionType, record.incomeAmount, 0)) as commissionAmount',
         'SUM(record.incomeAmount) as totalAmount',
-        'SUM(record.orderPrice) as totalSales'
+        'SUM(DISTINCT record.orderPrice) as totalSales'
       ])
       .where('record.createdAt >= :start', { start })
       .andWhere('record.createdAt <= :end', { end })
-      .andWhere('record.delFlag = :delFlag', { delFlag: false })
       .andWhere('record.status = :status', { status: true })
-      .setParameter('tipType', IncomeType.TIP)
-      .setParameter('commissionType', IncomeType.COMMISSION)
+      .andWhere('record.delFlag = :delFlag', { delFlag: false })
       .groupBy('record.agentId')
       .addGroupBy('DATE_FORMAT(record.createdAt, "%Y-%m-%d")')
       .orderBy('record.agentId', 'ASC')
       .addOrderBy('date', 'ASC')
 
     // 添加可选的过滤条件
-    if (agentId) {
+    if (agentId !== undefined) {
       queryBuilder = queryBuilder.andWhere('record.agentId = :agentId', { agentId })
     }
 
@@ -257,29 +290,32 @@ export class IncomeRecordsService {
     const agentStats = await queryBuilder.getRawMany()
 
     // 获取所有涉及的代理商ID
-    const agentIds = [...new Set(agentStats.map(stat => stat.agentId))].sort((a, b) => a - b)
+    const agentIdSet = new Set<number>()
+    agentStats.forEach((stat: any) => {
+      agentIdSet.add(Number(stat.agentId))
+    })
+    const agentIds = Array.from(agentIdSet).sort((a, b) => a - b)
 
     // 构建代理商数据映射
     const agentDataMap = new Map<number, Map<string, { tip: number; commission: number; total: number; sales: number }>>()
 
     // 初始化所有代理商的数据结构
-    agentIds.forEach(id => {
+    agentIds.forEach((id: number) => {
       agentDataMap.set(id, new Map())
     })
 
     // 填充统计数据
-    agentStats.forEach(stat => {
-      const agentId = stat.agentId
+    agentStats.forEach((stat: any) => {
+      const agentId = Number(stat.agentId)
       const date = stat.date
-      const tipAmount = Number(stat.tipAmount) || 0
-      const commissionAmount = Number(stat.commissionAmount) || 0
       const totalAmount = Number(stat.totalAmount) || 0
       const salesAmount = Number(stat.totalSales) || 0
 
       const dateMap = agentDataMap.get(agentId)!
+      // 使用 incomeAmount 统计,不区分 tip 和 commission
       dateMap.set(date, {
-        tip: tipAmount,
-        commission: commissionAmount,
+        tip: 0, // incomeAmount 不区分类型,设为0
+        commission: totalAmount, // 使用 incomeAmount 作为 commission
         total: totalAmount,
         sales: salesAmount
       })
@@ -338,20 +374,48 @@ export class IncomeRecordsService {
     const todaySales = totalSales.length > 0 ? [totalSales[totalSales.length - 1]] : [0]
 
     // 查询今日日活统计(基于会员的lastLoginAt字段)
-    const todayDAU = await this.memberRepository
+    // 需要根据 agentId 过滤:如果 agentId 为 0,查询 teamId = 0 的 member;如果 agentId 不为 0,通过 team 表关联查询
+    let todayDAUQuery = this.memberRepository
       .createQueryBuilder('member')
       .select('COUNT(DISTINCT member.userId) as dau')
       .where('member.lastLoginAt >= :today', { today: start })
       .andWhere('member.lastLoginAt <= :todayEnd', { todayEnd: end })
-      .getRawOne()
+
+    if (agentId !== undefined) {
+      if (agentId === 0) {
+        // 默认渠道:查询 teamId = 0 的 member
+        todayDAUQuery = todayDAUQuery.andWhere('member.teamId = 0')
+      } else {
+        // 指定渠道:通过 team 表关联查询 team.userId = agentId 的 member
+        todayDAUQuery = todayDAUQuery
+          .innerJoin('team', 'team', 'team.id = member.teamId')
+          .andWhere('team.userId = :agentId', { agentId })
+      }
+    }
+
+    const todayDAU = await todayDAUQuery.getRawOne()
 
     // 查询今日新增用户统计(基于会员的createdAt字段)
-    const todayNewUsers = await this.memberRepository
+    // 需要根据 agentId 过滤:如果 agentId 为 0,查询 teamId = 0 的 member;如果 agentId 不为 0,通过 team 表关联查询
+    let todayNewUsersQuery = this.memberRepository
       .createQueryBuilder('member')
       .select('COUNT(member.id) as newUsers')
       .where('member.createdAt >= :today', { today: start })
       .andWhere('member.createdAt <= :todayEnd', { todayEnd: end })
-      .getRawOne()
+
+    if (agentId !== undefined) {
+      if (agentId === 0) {
+        // 默认渠道:查询 teamId = 0 的 member
+        todayNewUsersQuery = todayNewUsersQuery.andWhere('member.teamId = 0')
+      } else {
+        // 指定渠道:通过 team 表关联查询 team.userId = agentId 的 member
+        todayNewUsersQuery = todayNewUsersQuery
+          .innerJoin('team', 'team', 'team.id = member.teamId')
+          .andWhere('team.userId = :agentId', { agentId })
+      }
+    }
+
+    const todayNewUsers = await todayNewUsersQuery.getRawOne()
 
     return {
       dates,

+ 386 - 0
src/services/landing-domain-pool.service.ts

@@ -0,0 +1,386 @@
+import { Repository, Like, In } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+import { LandingDomainPool, DomainType } from '../entities/landing-domain-pool.entity'
+import { TeamDomain } from '../entities/team-domain.entity'
+import { TeamMembers } from '../entities/team-members.entity'
+import { PaginationResponse } from '../dto/common.dto'
+import { CreateLandingDomainPoolBody, UpdateLandingDomainPoolBody, ListLandingDomainPoolQuery } from '../dto/landing-domain-pool.dto'
+import { TeamService } from './team.service'
+import { UserService } from './user.service'
+
+export class LandingDomainPoolService {
+  private landingDomainPoolRepository: Repository<LandingDomainPool>
+  private teamDomainRepository: Repository<TeamDomain>
+  private teamMembersRepository: Repository<TeamMembers>
+  private teamService: TeamService
+  private userService: UserService
+
+  constructor(app: FastifyInstance) {
+    this.landingDomainPoolRepository = app.dataSource.getRepository(LandingDomainPool)
+    this.teamDomainRepository = app.dataSource.getRepository(TeamDomain)
+    this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
+    this.teamService = new TeamService(app)
+    this.userService = new UserService(app)
+  }
+
+  async create(data: CreateLandingDomainPoolBody): Promise<LandingDomainPool> {
+    await this.teamService.findById(data.teamId)
+
+    // 如果提供了 userId,可能是 TeamMembers 的 id,需要转换为 user 的 id
+    let actualUserId = data.userId
+    if (data.userId !== undefined && data.userId !== null) {
+      // 先尝试作为 TeamMembers 的 id 查找
+      const teamMember = await this.teamMembersRepository.findOne({
+        where: { id: data.userId }
+      })
+      if (teamMember) {
+        // 如果找到 TeamMembers,使用其 userId(即 user 的 id)
+        actualUserId = teamMember.userId
+      }
+      // 如果找不到 TeamMembers,则假设传入的已经是 user 的 id,验证用户是否存在
+      if (actualUserId !== null && actualUserId !== undefined) {
+        await this.userService.findById(actualUserId)
+      }
+    }
+
+    const existingDomain = await this.landingDomainPoolRepository.findOne({
+      where: { domain: data.domain }
+    })
+    if (existingDomain) {
+      throw new Error('域名已存在')
+    }
+
+    const landingDomainPool = this.landingDomainPoolRepository.create({
+      ...data,
+      userId: actualUserId,
+      domainType: data.domainType || DomainType.LANDING
+    })
+    return this.landingDomainPoolRepository.save(landingDomainPool)
+  }
+
+  async createBatch(
+    data: CreateLandingDomainPoolBody
+  ): Promise<{ success: LandingDomainPool[]; failed: { domain: string; error: string }[] }> {
+    await this.teamService.findById(data.teamId)
+
+    // 如果提供了 userId,可能是 TeamMembers 的 id,需要转换为 user 的 id
+    let actualUserId = data.userId
+    if (data.userId !== undefined && data.userId !== null) {
+      // 先尝试作为 TeamMembers 的 id 查找
+      const teamMember = await this.teamMembersRepository.findOne({
+        where: { id: data.userId }
+      })
+      if (teamMember) {
+        // 如果找到 TeamMembers,使用其 userId(即 user 的 id)
+        actualUserId = teamMember.userId
+      }
+      // 如果找不到 TeamMembers,则假设传入的已经是 user 的 id,保持不变
+    }
+
+    // 解析域名字符串,支持逗号和换行分隔
+    const domains = this.parseDomains(data.domain)
+
+    if (domains.length === 0) {
+      throw new Error('没有有效的域名')
+    }
+
+    const success: LandingDomainPool[] = []
+    const failed: { domain: string; error: string }[] = []
+    
+    // 检查已存在的域名
+    const existingDomains = await this.landingDomainPoolRepository.find({
+      where: { domain: In(domains) }
+    })
+    const existingDomainSet = new Set(existingDomains.map(d => d.domain))
+
+    // 批量创建域名
+    const domainsToCreate = domains.filter(domain => !existingDomainSet.has(domain))
+
+    if (domainsToCreate.length > 0) {
+      const landingDomainPools = domainsToCreate.map(domain =>
+        this.landingDomainPoolRepository.create({
+          teamId: data.teamId,
+          domain: domain.trim(),
+          description: data.description,
+          userId: actualUserId,
+          domainType: data.domainType || DomainType.LANDING
+        })
+      )
+
+      const savedDomains = await this.landingDomainPoolRepository.save(landingDomainPools)
+      success.push(...savedDomains)
+    }
+
+    // 记录失败的域名(已存在的域名)
+    domains.forEach(domain => {
+      if (existingDomainSet.has(domain)) {
+        failed.push({ domain, error: '域名已存在' })
+      }
+    })
+
+    return { success, failed }
+  }
+
+  private parseDomains(domainString: string): string[] {
+    // 支持中英文逗号、分号和换行分隔,去除空白字符
+    return domainString
+      .split(/[,;\n\r]+/)
+      .map(domain => domain.trim())
+      .filter(domain => domain.length > 0)
+  }
+
+  async findById(id: number): Promise<LandingDomainPool> {
+    return this.landingDomainPoolRepository.findOneOrFail({ where: { id } })
+  }
+
+  async findAll(query: ListLandingDomainPoolQuery): Promise<PaginationResponse<LandingDomainPool>> {
+    const { page, size, id, teamId, domain, userId, domainType } = query
+
+    const where: any = {}
+
+    if (id) {
+      where.id = id
+    }
+
+    if (teamId) {
+      where.teamId = teamId
+    }
+
+    if (domain) {
+      where.domain = Like(`%${domain}%`)
+    }
+
+    if (userId !== undefined && userId !== null) {
+      where.userId = userId
+    }
+
+    if (domainType !== undefined) {
+      where.domainType = domainType
+    }
+
+    const [landingDomainPools, total] = await this.landingDomainPoolRepository.findAndCount({
+      where,
+      skip: (Number(page) || 0) * (Number(size) || 20),
+      take: Number(size) || 20,
+      order: { createdAt: 'DESC' }
+    })
+
+    return {
+      content: landingDomainPools,
+      metadata: {
+        total: Number(total),
+        page: Number(page) || 0,
+        size: Number(size) || 20
+      }
+    }
+  }
+
+  async update(data: UpdateLandingDomainPoolBody): Promise<LandingDomainPool> {
+    const { id, ...updateData } = data
+
+    // 先获取现有记录
+    const existingDomain = await this.findById(id)
+
+    // 如果要更新域名,检查是否重复
+    if (updateData.domain && updateData.domain !== existingDomain.domain) {
+      const duplicateDomain = await this.landingDomainPoolRepository.findOne({
+        where: { domain: updateData.domain }
+      })
+      if (duplicateDomain) {
+        throw new Error('域名已存在')
+      }
+    }
+
+    // 如果要更新团队ID,验证团队是否存在
+    if (updateData.teamId) {
+      await this.teamService.findById(updateData.teamId)
+    }
+
+    // 如果要更新 userId,可能是 TeamMembers 的 id,需要转换为 user 的 id
+    let actualUserId = updateData.userId
+    if (updateData.userId !== undefined && updateData.userId !== null) {
+      // 先尝试作为 TeamMembers 的 id 查找
+      const teamMember = await this.teamMembersRepository.findOne({
+        where: { id: updateData.userId }
+      })
+      if (teamMember) {
+        // 如果找到 TeamMembers,使用其 userId(即 user 的 id)
+        actualUserId = teamMember.userId
+      }
+      // 如果找不到 TeamMembers,则假设传入的已经是 user 的 id,保持不变
+      // 验证用户是否存在
+      if (actualUserId !== null && actualUserId !== undefined) {
+        await this.userService.findById(actualUserId)
+      }
+    }
+
+    // 构建符合 TypeORM 要求的更新对象
+    const updateEntity: Partial<LandingDomainPool> = {}
+    if (updateData.domain !== undefined) {
+      updateEntity.domain = updateData.domain
+    }
+    if (updateData.description !== undefined) {
+      updateEntity.description = updateData.description
+    }
+    if (updateData.teamId !== undefined) {
+      updateEntity.teamId = updateData.teamId
+    }
+    if (updateData.userId !== undefined) {
+      updateEntity.userId = actualUserId
+    }
+    if (updateData.domainType !== undefined) {
+      updateEntity.domainType = updateData.domainType
+    }
+
+    await this.landingDomainPoolRepository.update(id, updateEntity)
+    return this.findById(id)
+  }
+
+  async delete(id: number): Promise<void> {
+    await this.landingDomainPoolRepository.delete(id)
+  }
+
+  async findByTeamId(teamId: number): Promise<LandingDomainPool[]> {
+    return await this.landingDomainPoolRepository.find({
+      where: { teamId },
+      order: { createdAt: 'DESC' }
+    })
+  }
+
+  async findAllGroupedByTeam(query?: ListLandingDomainPoolQuery): Promise<Record<number, LandingDomainPool[]>> {
+    const { id, teamId, domain, userId, domainType } = query || {}
+
+    const where: any = {}
+
+    if (id) {
+      where.id = id
+    }
+
+    if (teamId) {
+      where.teamId = teamId
+    }
+
+    if (domain) {
+      where.domain = Like(`%${domain}%`)
+    }
+
+    if (userId !== undefined && userId !== null) {
+      where.userId = userId
+    }
+
+    if (domainType !== undefined) {
+      where.domainType = domainType
+    }
+
+    const landingDomainPools = await this.landingDomainPoolRepository.find({
+      where,
+      order: { teamId: 'ASC', createdAt: 'DESC' }
+    })
+
+    // 按 teamId 分组
+    const groupedData: Record<number, LandingDomainPool[]> = {}
+    landingDomainPools.forEach(domain => {
+      if (!groupedData[domain.teamId]) {
+        groupedData[domain.teamId] = []
+      }
+      groupedData[domain.teamId].push(domain)
+    })
+
+    return groupedData
+  }
+
+  /**
+   * 根据域名(team-domain)获取落地域名池列表
+   * 如果域名绑定到个人(teamMemberId不为空),返回该个人的所有落地域名
+   * 如果域名绑定到团队(teamMemberId为空),返回该团队的所有落地域名
+   * @param domain team-domain中的域名
+   * @returns 落地域名池列表
+   */
+  async findByTeamDomain(domain: string): Promise<LandingDomainPool[]> {
+    // 根据域名查找team-domain记录
+    const teamDomain = await this.teamDomainRepository.findOne({
+      where: { domain }
+    })
+
+    if (!teamDomain) {
+      throw new Error('域名不存在')
+    }
+
+    // 如果域名绑定到个人(teamMemberId不为空)
+    if (teamDomain.teamMemberId !== null && teamDomain.teamMemberId !== undefined) {
+      // 查找团队成员信息
+      const teamMember = await this.teamMembersRepository.findOne({
+        where: { id: teamDomain.teamMemberId }
+      })
+
+      if (!teamMember) {
+        throw new Error('团队成员不存在')
+      }
+
+      // 返回该个人(userId)的所有落地域名
+      return await this.landingDomainPoolRepository.find({
+        where: { userId: teamMember.userId },
+        order: { createdAt: 'DESC' }
+      })
+    } else {
+      // 如果域名绑定到团队(teamMemberId为空),返回该团队的所有落地域名
+      return await this.findByTeamId(teamDomain.teamId)
+    }
+  }
+
+  /**
+   * 根据当前落地域名获取留存域名
+   * 根据传入的落地域名,查找对应的落地域名池记录,然后返回相同团队和用户的留存域名
+   * 如果没有找到留存域名,则返回落地域名本身
+   * 如果没有找到落地域名,则返回输入的域名(作为临时落地域名对象)
+   * @param domain 落地域名
+   * @returns 留存域名列表,如果没有留存域名则返回落地域名,如果没有落地域名则返回输入的域名
+   */
+  async getRetentionDomainsByLandingDomain(domain: string): Promise<LandingDomainPool[]> {
+    // 根据域名查找落地域名池记录(domainType = 'landing')
+    const landingDomain = await this.landingDomainPoolRepository.findOne({
+      where: { domain, domainType: DomainType.LANDING }
+    })
+
+    // 如果没有找到落地域名,返回输入的域名(作为临时落地域名对象)
+    if (!landingDomain) {
+      const fallbackDomain = this.landingDomainPoolRepository.create({
+        domain: domain,
+        domainType: DomainType.LANDING,
+        teamId: 0,
+        userId: undefined,
+        description: undefined,
+        createdAt: new Date(),
+        updatedAt: new Date()
+      })
+      return [fallbackDomain]
+    }
+
+    // 构建查询条件:相同的 teamId 和 userId,且 domainType = 'retention'
+    const where: any = {
+      teamId: landingDomain.teamId,
+      domainType: DomainType.RETENTION
+    }
+
+    // 如果落地域名绑定了用户,则只返回该用户的留存域名
+    // 如果没有绑定用户,则返回该团队的所有留存域名
+    if (landingDomain.userId !== null && landingDomain.userId !== undefined) {
+      where.userId = landingDomain.userId
+    }
+
+    // 查询留存域名列表
+    const retentionDomains = await this.landingDomainPoolRepository.find({
+      where,
+      order: { createdAt: 'DESC' }
+    })
+
+    // 如果没有找到留存域名,返回落地域名本身
+    if (retentionDomains.length === 0) {
+      return [landingDomain]
+    }
+
+    // 返回留存域名列表
+    return retentionDomains
+  }
+}
+

+ 187 - 13
src/services/member.service.ts

@@ -7,6 +7,7 @@ import * as randomstring from 'randomstring'
 import { Team } from '../entities/team.entity'
 import { TeamDomain } from '../entities/team-domain.entity'
 import { TeamMembers } from '../entities/team-members.entity'
+import { LandingDomainPool } from '../entities/landing-domain-pool.entity'
 import bcrypt from 'bcryptjs'
 
 export class MemberService {
@@ -30,7 +31,7 @@ export class MemberService {
     this.dataSource = app.dataSource
   }
 
-  async createGuest(code?: string, domain?: string, ip?: string): Promise<User> {
+  async createGuest(code?: string, domain?: string, ip?: string, landingDomain?: string): Promise<User> {
     return await this.dataSource.transaction(async manager => {
       const randomSuffix = randomstring.generate({
         length: 10,
@@ -49,6 +50,9 @@ export class MemberService {
       let teamId = 0
       let domainId = 0
 
+      // 校验顺序:推广码 -> 跳转域名 -> 落地域名
+      
+      // 1. 优先使用推广码
       if (code && code.trim() !== '') {
         // 先尝试查找团队成员的推广码
         const teamMember = await manager.findOne(TeamMembers, { where: { promoCode: code } })
@@ -64,7 +68,10 @@ export class MemberService {
             teamId = team.id
           }
         }
-      } else if (domain) {
+      }
+      
+      // 2. 如果推广码没有找到,使用跳转域名(team-domain)
+      if (parentId === 1 && teamId === 0 && domain) {
         let domainName = domain
         try {
           if (domain.includes('://')) {
@@ -79,19 +86,99 @@ export class MemberService {
 
         const teamDomain = await manager.findOne(TeamDomain, { where: { domain: domainName } })
         if (teamDomain) {
-          const team = await manager.findOne(Team, { where: { id: teamDomain.teamId } })
-          if (team) {
-            parentId = team.userId
-            teamId = team.id
+          // 如果domain绑定到teamMember,使用teamMember的userId作为parentId
+          // 这样在分润时可以从teamMember向上查找到team
+          if (teamDomain.teamMemberId) {
+            const teamMember = await manager.findOne(TeamMembers, { where: { id: teamDomain.teamMemberId } })
+            if (teamMember) {
+              parentId = teamMember.userId
+              teamId = teamMember.teamId
+            }
+          } else {
+            // 如果domain只绑定到team,使用team的userId作为parentId
+            const team = await manager.findOne(Team, { where: { id: teamDomain.teamId } })
+            if (team) {
+              parentId = team.userId
+              teamId = team.id
+            }
           }
           domainId = teamDomain.id
         }
       }
+      
+      // 3. 如果跳转域名没有找到,使用落地域名(landing-domain)
+      if (parentId === 1 && teamId === 0 && landingDomain && landingDomain.trim() !== '') {
+        const landingDomainPoolRepository = manager.getRepository(LandingDomainPool)
+        const landingDomainRecord = await landingDomainPoolRepository.findOne({
+          where: { domain: landingDomain.trim() }
+        })
+        
+        if (landingDomainRecord) {
+          // 如果找到落地域名记录,使用对应的团队ID
+          teamId = landingDomainRecord.teamId
+          // 根据 teamId 查找团队,获取 userId 作为 parentId
+          const foundTeam = await manager.findOne(Team, { where: { id: teamId } })
+          if (foundTeam) {
+            parentId = foundTeam.userId
+          }
+        }
+      }
+      
+      // 4. 如果以上都没有找到有效注册信息,统一分给当天注册最多的域名
+      if (parentId === 1 && teamId === 0) {
+        // 查询当天注册最多的域名
+        const today = new Date()
+        today.setHours(0, 0, 0, 0)
+        const tomorrow = new Date(today)
+        tomorrow.setDate(tomorrow.getDate() + 1)
+        
+        // 统计当天每个域名的注册数量
+        const domainCounts = await manager
+          .createQueryBuilder(Member, 'member')
+          .select('member.domainId', 'domainId')
+          .addSelect('COUNT(member.id)', 'count')
+          .where('member.createdAt >= :today', { today })
+          .andWhere('member.createdAt < :tomorrow', { tomorrow })
+          .andWhere('member.domainId > 0')
+          .groupBy('member.domainId')
+          .orderBy('count', 'DESC')
+          .limit(1)
+          .getRawMany()
+        
+        if (domainCounts.length > 0 && domainCounts[0].domainId) {
+          const mostRegisteredDomainId = domainCounts[0].domainId
+          const teamDomain = await manager.findOne(TeamDomain, { where: { id: mostRegisteredDomainId } })
+          
+          if (teamDomain) {
+            domainId = mostRegisteredDomainId
+            
+            // 如果域名绑定到团队成员
+            if (teamDomain.teamMemberId) {
+              const teamMember = await manager.findOne(TeamMembers, { where: { id: teamDomain.teamMemberId } })
+              if (teamMember) {
+                parentId = teamMember.userId
+                teamId = teamMember.teamId
+              }
+            } else if (teamDomain.teamId) {
+              // 如果域名只绑定到团队
+              const foundTeam = await manager.findOne(Team, { where: { id: teamDomain.teamId } })
+              if (foundTeam) {
+                parentId = foundTeam.userId
+                teamId = foundTeam.id
+              }
+            }
+          }
+        }
+      }
 
+      const defaultPassword = 'password123'
+      const hashedPassword = await bcrypt.hash(defaultPassword, 10)
+      
       const user = manager.create(User, {
         name: finalGuestName,
         role: UserRole.USER,
-        parentId
+        parentId,
+        password: hashedPassword
       })
 
       const savedUser = await manager.save(user)
@@ -342,7 +429,8 @@ export class MemberService {
     phone?: string,
     code?: string,
     ip?: string,
-    memberCode?: string
+    memberCode?: string,
+    landingDomain?: string
   ): Promise<{ user: User; member: Member }> {
     return await this.dataSource.transaction(async manager => {
       // 检查用户名是否已存在
@@ -368,14 +456,14 @@ export class MemberService {
       }
 
       // 获取推荐团队或根据IP查找历史注册信息
+      // 校验顺序:推广码 -> 落地域名 -> IP历史记录
       let team = null
       let parentId = 1
       let teamId = 0
       let domainId = 0
 
-      // 优先使用 memberCode(团队成员推广码),如果没有则使用 code(团队推广码
+      // 1. 优先使用推广码(memberCode 或 code
       const promoCode = memberCode || code
-
       if (promoCode && promoCode.trim() !== '') {
         // 如果提供了 memberCode,优先查找团队成员的推广码
         if (memberCode && memberCode.trim() !== '') {
@@ -397,7 +485,28 @@ export class MemberService {
             domainId = 0
           }
         }
-      } else if (ip && ip !== 'unknown') {
+      }
+      
+      // 2. 如果推广码没有找到,使用落地域名(landing-domain)
+      if (parentId === 1 && teamId === 0 && landingDomain && landingDomain.trim() !== '') {
+        const landingDomainPoolRepository = manager.getRepository(LandingDomainPool)
+        const landingDomainRecord = await landingDomainPoolRepository.findOne({
+          where: { domain: landingDomain.trim() }
+        })
+        
+        if (landingDomainRecord) {
+          // 如果找到落地域名记录,使用对应的团队ID
+          teamId = landingDomainRecord.teamId
+          // 根据 teamId 查找团队,获取 userId 作为 parentId
+          const foundTeam = await manager.findOne(Team, { where: { id: teamId } })
+          if (foundTeam) {
+            parentId = foundTeam.userId
+          }
+        }
+      }
+      
+      // 3. 如果以上都没有找到,根据IP查找历史注册信息
+      if (parentId === 1 && teamId === 0 && ip && ip !== 'unknown') {
         // 如果没有推广参数,检查注册IP是否之前注册过账号
         const existingMemberByIp = await manager.findOne(Member, {
           where: { ip },
@@ -409,8 +518,26 @@ export class MemberService {
           teamId = existingMemberByIp.teamId || 0
           domainId = existingMemberByIp.domainId || 0
 
-          // 根据teamId查找team,获取userId作为parentId(agentId)
-          if (teamId > 0) {
+          // 如果domainId存在,检查domain是否绑定到teamMember
+          if (domainId > 0) {
+            const teamDomain = await manager.findOne(TeamDomain, { where: { id: domainId } })
+            if (teamDomain && teamDomain.teamMemberId) {
+              // 如果domain绑定到teamMember,使用teamMember的userId作为parentId
+              const teamMember = await manager.findOne(TeamMembers, { where: { id: teamDomain.teamMemberId } })
+              if (teamMember) {
+                parentId = teamMember.userId
+                teamId = teamMember.teamId
+              }
+            } else if (teamDomain && teamDomain.teamId) {
+              // 如果domain只绑定到team,使用team的userId作为parentId
+              const existingTeam = await manager.findOne(Team, { where: { id: teamDomain.teamId } })
+              if (existingTeam) {
+                parentId = existingTeam.userId
+                teamId = existingTeam.id
+              }
+            }
+          } else if (teamId > 0) {
+            // 如果没有domainId,根据teamId查找team,获取userId作为parentId(agentId)
             const existingTeam = await manager.findOne(Team, { where: { id: teamId } })
             if (existingTeam) {
               parentId = existingTeam.userId
@@ -418,6 +545,53 @@ export class MemberService {
           }
         }
       }
+      
+      // 4. 如果以上都没有找到有效注册信息,统一分给当天注册最多的域名
+      if (parentId === 1 && teamId === 0) {
+        // 查询当天注册最多的域名
+        const today = new Date()
+        today.setHours(0, 0, 0, 0)
+        const tomorrow = new Date(today)
+        tomorrow.setDate(tomorrow.getDate() + 1)
+        
+        // 统计当天每个域名的注册数量
+        const domainCounts = await manager
+          .createQueryBuilder(Member, 'member')
+          .select('member.domainId', 'domainId')
+          .addSelect('COUNT(member.id)', 'count')
+          .where('member.createdAt >= :today', { today })
+          .andWhere('member.createdAt < :tomorrow', { tomorrow })
+          .andWhere('member.domainId > 0')
+          .groupBy('member.domainId')
+          .orderBy('count', 'DESC')
+          .limit(1)
+          .getRawMany()
+        
+        if (domainCounts.length > 0 && domainCounts[0].domainId) {
+          const mostRegisteredDomainId = domainCounts[0].domainId
+          const teamDomain = await manager.findOne(TeamDomain, { where: { id: mostRegisteredDomainId } })
+          
+          if (teamDomain) {
+            domainId = mostRegisteredDomainId
+            
+            // 如果域名绑定到团队成员
+            if (teamDomain.teamMemberId) {
+              const teamMember = await manager.findOne(TeamMembers, { where: { id: teamDomain.teamMemberId } })
+              if (teamMember) {
+                parentId = teamMember.userId
+                teamId = teamMember.teamId
+              }
+            } else if (teamDomain.teamId) {
+              // 如果域名只绑定到团队
+              const foundTeam = await manager.findOne(Team, { where: { id: teamDomain.teamId } })
+              if (foundTeam) {
+                parentId = foundTeam.userId
+                teamId = foundTeam.id
+              }
+            }
+          }
+        }
+      }
 
       // 创建用户
       const hashedPassword = await bcrypt.hash(password, 10)

+ 664 - 0
src/services/multi-level-commission.service.ts

@@ -0,0 +1,664 @@
+import { FastifyInstance } from 'fastify'
+import { Repository, DataSource } from 'typeorm'
+import { User } from '../entities/user.entity'
+import { Team } from '../entities/team.entity'
+import { TeamMembers } from '../entities/team-members.entity'
+import { IncomeRecords } from '../entities/income-records.entity'
+import { AgentCommissionIndex } from '../entities/agent-commission-index.entity'
+import { TeamDomain } from '../entities/team-domain.entity'
+import { Member } from '../entities/member.entity'
+import Decimal from 'decimal.js'
+
+export interface CommissionDetail {
+  level: number
+  agentId: number
+  rate: number
+  amount: number
+}
+
+export interface ParentChainItem {
+  level: number
+  userId: number
+  commissionRate: number | string  // 数据库返回的 decimal 类型可能是字符串
+  type: 'team' | 'teamMember'
+}
+
+export class MultiLevelCommissionService {
+  private app: FastifyInstance
+  private dataSource: DataSource
+  private userRepository: Repository<User>
+  private teamRepository: Repository<Team>
+  private teamMembersRepository: Repository<TeamMembers>
+  private incomeRecordsRepository: Repository<IncomeRecords>
+  private indexRepository: Repository<AgentCommissionIndex>
+  private teamDomainRepository: Repository<TeamDomain>
+  private memberRepository: Repository<Member>
+
+  constructor(app: FastifyInstance) {
+    this.app = app
+    this.dataSource = app.dataSource
+    this.userRepository = app.dataSource.getRepository(User)
+    this.teamRepository = app.dataSource.getRepository(Team)
+    this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
+    this.incomeRecordsRepository = app.dataSource.getRepository(IncomeRecords)
+    this.indexRepository = app.dataSource.getRepository(AgentCommissionIndex)
+    this.teamDomainRepository = app.dataSource.getRepository(TeamDomain)
+    this.memberRepository = app.dataSource.getRepository(Member)
+  }
+
+  /**
+   * 获取用户的所有上级代理(按层级排序,支持无限层级)
+   * 逻辑:
+   * 1. 从 user 的 member 关联表里的 domainId 查询到 teamMember
+   * 2. 从 teamMember 的 parentId 开始向上查询其上级代理
+   * 3. 查到 parentId 为空为止
+   * 4. 此时添加最后一个团队层级,从 team 表获取
+   */
+  async getUserParentChain(
+    userId: number,
+    maxLevel?: number,
+    member?: Member | null
+  ): Promise<ParentChainItem[]> {
+    const chain: ParentChainItem[] = []
+    let level = 1
+
+    this.app.log.debug({ userId, domainId: member?.domainId, maxLevel }, 'getUserParentChain: 开始查找用户上级代理链')
+
+    // 如果没有提供 member 或 domainId 无效,返回空链
+    if (!member || !member.domainId || member.domainId <= 0) {
+      this.app.log.debug({ userId }, 'getUserParentChain: member 或 domainId 无效,返回空链')
+      return chain
+    }
+
+    // 1. 通过 domainId 查询 teamDomain
+    const teamDomain = await this.teamDomainRepository.findOne({
+      where: { id: member.domainId }
+    })
+
+    if (!teamDomain || !teamDomain.teamMemberId || teamDomain.teamMemberId <= 0) {
+      this.app.log.debug({ userId, domainId: member.domainId, teamDomain: teamDomain ? { id: teamDomain.id, teamMemberId: teamDomain.teamMemberId } : null }, 'getUserParentChain: teamDomain 无效,返回空链')
+      return chain
+    }
+
+    this.app.log.debug({ userId, domainId: member.domainId, teamMemberId: teamDomain.teamMemberId }, 'getUserParentChain: 找到 teamDomain')
+
+    // 2. 通过 teamDomain.teamMemberId 查询到初始 teamMember
+    let currentTeamMember = await this.teamMembersRepository.findOne({
+      where: { id: teamDomain.teamMemberId }
+    })
+
+    if (!currentTeamMember) {
+      this.app.log.debug({ userId, teamMemberId: teamDomain.teamMemberId }, 'getUserParentChain: 未找到初始 teamMember,返回空链')
+      return chain
+    }
+
+    this.app.log.debug({ userId, teamMemberId: currentTeamMember.id, teamMemberUserId: currentTeamMember.userId, commissionRate: currentTeamMember.commissionRate }, 'getUserParentChain: 找到初始 teamMember,开始向上查找')
+
+    // 3. 从 teamMember 开始,通过 parentId 向上循环查找所有上级代理
+    while (currentTeamMember) {
+      // 检查最大层级限制
+      if (maxLevel !== undefined && level > maxLevel) {
+        this.app.log.debug({ userId, level, maxLevel }, 'getUserParentChain: 达到最大层级限制,停止查找')
+        break
+      }
+
+      // 添加到链中(即使 commissionRate 为 0 也要添加,因为调整后可能不为 0)
+      // 确保 commissionRate 转换为数字(数据库返回的 decimal 可能是字符串)
+      const commissionRate = typeof currentTeamMember.commissionRate === 'string'
+        ? parseFloat(currentTeamMember.commissionRate)
+        : Number(currentTeamMember.commissionRate)
+      
+      chain.push({
+        level,
+        userId: currentTeamMember.userId,
+        commissionRate: commissionRate,
+        type: 'teamMember'
+      })
+
+      this.app.log.debug({ 
+        userId, 
+        level, 
+        teamMemberId: currentTeamMember.id,
+        teamMemberUserId: currentTeamMember.userId, 
+        commissionRate: currentTeamMember.commissionRate,
+        parentId: currentTeamMember.parentId 
+      }, 'getUserParentChain: 添加 teamMember 到链中')
+
+      // 添加完当前 teamMember 后,递增 level
+      level++
+
+      // 如果 parentId 为空,说明已经到了最顶层,需要查找 team
+      if (!currentTeamMember.parentId) {
+        this.app.log.debug({ userId, teamMemberId: currentTeamMember.id, teamId: currentTeamMember.teamId }, 'getUserParentChain: teamMember parentId 为空,根据 teamId 查找对应的 team')
+        // 4. 根据 teamMember.teamId 查找对应的 team
+        const team = await this.teamRepository.findOne({
+          where: { id: currentTeamMember.teamId }
+        })
+
+        if (team) {
+          // 确保 commissionRate 转换为数字
+          const teamCommissionRate = typeof team.commissionRate === 'string'
+            ? parseFloat(team.commissionRate)
+            : Number(team.commissionRate)
+          
+          chain.push({
+            level,
+            userId: team.userId,
+            commissionRate: teamCommissionRate,
+            type: 'team'
+          })
+          this.app.log.debug({ userId, level, teamId: team.id, teamUserId: team.userId, commissionRate: teamCommissionRate }, 'getUserParentChain: 找到并添加 team 到链中')
+        } else {
+          this.app.log.debug({ userId, teamId: currentTeamMember.teamId }, 'getUserParentChain: 未找到对应的 team')
+        }
+        break
+      }
+
+      // 继续向上查找上级 teamMember
+      const parentId = currentTeamMember.parentId
+      currentTeamMember = await this.teamMembersRepository.findOne({
+        where: { id: parentId }
+      })
+
+      if (!currentTeamMember) {
+        this.app.log.debug({ userId, parentId }, 'getUserParentChain: 未找到上级 teamMember,停止查找')
+      }
+    }
+
+    this.app.log.info({ userId, chainLength: chain.length, chain: chain.map(item => ({ level: item.level, userId: item.userId, commissionRate: item.commissionRate, type: item.type })) }, 'getUserParentChain: 查找完成,返回代理链')
+
+    return chain
+  }
+
+  /**
+   * 获取第一个 team 层的分润信息(用于 incomeAmount 统计)
+   * 从用户的 parentId 向上查找,找到第一个 type='team' 的分润信息
+   * 使用调整后的分润比例(team比例 - 下级比例)
+   */
+  async getFirstTeamCommission(
+    userId: number,
+    orderPrice: number,
+    member?: Member | null
+  ): Promise<{ agentId: number; commissionRate: number; amount: number } | null> {
+    this.app.log.debug({ userId, orderPrice }, 'getFirstTeamCommission: 开始查找第一个 team 层的分润信息')
+    
+    const parentChain = await this.getUserParentChain(userId, undefined, member)
+    
+    if (parentChain.length === 0) {
+      this.app.log.debug({ userId }, 'getFirstTeamCommission: 代理链为空,返回 null')
+      return null
+    }
+
+    // 按 level 排序(从小到大)
+    const sortedChain = [...parentChain].sort((a, b) => a.level - b.level)
+    
+    // 查找第一个 type='team' 的项
+    const firstTeamIndex = sortedChain.findIndex(item => item.type === 'team')
+    
+    if (firstTeamIndex === -1) {
+      this.app.log.debug({ userId }, 'getFirstTeamCommission: 未找到 team 类型的项,返回 null')
+      return null
+    }
+
+    const firstTeam = sortedChain[firstTeamIndex]
+    // 确保 commissionRate 转换为数字
+    const firstTeamRate = typeof firstTeam.commissionRate === 'string'
+      ? parseFloat(firstTeam.commissionRate)
+      : Number(firstTeam.commissionRate)
+    
+    this.app.log.debug({ userId, firstTeamLevel: firstTeam.level, firstTeamUserId: firstTeam.userId, firstTeamCommissionRate: firstTeamRate }, 'getFirstTeamCommission: 找到第一个 team')
+    
+    // 计算调整后的比例:team比例 - 下一级的比例
+    let adjustedRate = firstTeamRate
+    
+    // 查找下一级(level 更大的项,且 commissionRate > 0)
+    const nextItem = sortedChain.find((item, index) => {
+      if (index <= firstTeamIndex) return false
+      const itemRate = typeof item.commissionRate === 'string'
+        ? parseFloat(item.commissionRate)
+        : Number(item.commissionRate)
+      return itemRate > 0
+    })
+    
+    if (nextItem) {
+      const nextItemRate = typeof nextItem.commissionRate === 'string'
+        ? parseFloat(nextItem.commissionRate)
+        : Number(nextItem.commissionRate)
+      adjustedRate = Math.max(0, firstTeamRate - nextItemRate)
+      this.app.log.debug({ userId, originalRate: firstTeamRate, nextItemRate, adjustedRate }, 'getFirstTeamCommission: 计算调整后的比例')
+    } else {
+      this.app.log.debug({ userId, adjustedRate }, 'getFirstTeamCommission: 没有下一级,使用原比例')
+    }
+    
+    if (adjustedRate > 0) {
+      const orderPriceDecimal = new Decimal(orderPrice)
+      const rateDecimal = new Decimal(adjustedRate)
+      const commissionAmount = orderPriceDecimal.mul(rateDecimal).div(100).toNumber()
+      
+      this.app.log.info({ userId, agentId: firstTeam.userId, commissionRate: adjustedRate, amount: commissionAmount }, 'getFirstTeamCommission: 计算完成')
+      
+      return {
+        agentId: firstTeam.userId,
+        commissionRate: adjustedRate,  // 使用调整后的比例
+        amount: commissionAmount
+      }
+    }
+    
+    this.app.log.debug({ userId, adjustedRate }, 'getFirstTeamCommission: 调整后的比例为 0,返回 null')
+    return null
+  }
+
+  /**
+   * 计算多级分润(支持无限层级)
+   * 从 team.commissionRate 或 team_members.commissionRate 获取分润比例
+   * 优先使用 team_members,如果找不到则使用 team
+   * 优先通过 domainId 查找 teamDomain 绑定关系
+   * 
+   * 分润比例计算规则:
+   * - 每一级的分润比例 = 自己的比例 - 上一级的比例(level 更小的)
+   * - 第一级(level 最小的)保持原比例
+   * 
+   * 例如:
+   * - Level 1: 30%, Level 2: 50%, Level 3: 70%
+   * - 调整后:Level 1: 30% (第一级), Level 2: 20% (50-30), Level 3: 20% (70-50)
+   */
+  async calculateCommission(
+    userId: number,
+    orderPrice: number,
+    member?: Member | null
+  ): Promise<CommissionDetail[]> {
+    this.app.log.debug({ userId, orderPrice }, 'calculateCommission: 开始计算多级分润')
+    
+    // 1. 获取用户上级关系链(包含 team 和 team_members)
+    // 优先通过 domainId 查找 teamDomain 绑定关系
+    const parentChain = await this.getUserParentChain(userId, undefined, member)
+
+    if (parentChain.length === 0) {
+      this.app.log.debug({ userId }, 'calculateCommission: 代理链为空,返回空数组')
+      return []
+    }
+
+    this.app.log.debug({ userId, chainLength: parentChain.length, chain: parentChain.map(item => ({ level: item.level, userId: item.userId, commissionRate: item.commissionRate, type: item.type })) }, 'calculateCommission: 获取到代理链')
+
+    // 2. 按 level 排序(从小到大)
+    const sortedChain = [...parentChain].sort((a, b) => a.level - b.level)
+
+    // 3. 计算每一级的分润比例(当前比例 - 上一级的比例)
+    const adjustedRates: Map<number, number> = new Map()
+    
+    this.app.log.debug({ userId }, 'calculateCommission: 开始计算调整后的分润比例')
+    
+    // 从第一级开始向后计算
+    for (let i = 0; i < sortedChain.length; i++) {
+      const current = sortedChain[i]
+      // 确保 commissionRate 转换为数字
+      const currentRate = typeof current.commissionRate === 'string' 
+        ? parseFloat(current.commissionRate) 
+        : Number(current.commissionRate)
+      
+      if (i === 0) {
+        // 第一级(level 最小的):保持原比例
+        adjustedRates.set(current.level, currentRate)
+        this.app.log.debug({ 
+          userId, 
+          level: current.level, 
+          commissionRate: currentRate 
+        }, 'calculateCommission: 第一级,保持原比例')
+      } else {
+        // 其他级:当前比例 - 上一级的比例
+        const prev = sortedChain[i - 1]
+        const prevRate = typeof prev.commissionRate === 'string'
+          ? parseFloat(prev.commissionRate)
+          : Number(prev.commissionRate)
+        const adjustedRate = Math.max(0, currentRate - prevRate)
+        adjustedRates.set(current.level, adjustedRate)
+        this.app.log.debug({ 
+          userId, 
+          level: current.level, 
+          currentRate, 
+          prevLevel: prev.level,
+          prevRate, 
+          adjustedRate 
+        }, 'calculateCommission: 计算调整后的比例')
+      }
+    }
+
+    // 4. 计算每一级的分润金额
+    const result: CommissionDetail[] = []
+    const orderPriceDecimal = new Decimal(orderPrice)
+
+    sortedChain.forEach(({ level, userId: agentId, commissionRate }) => {
+      const adjustedRate = adjustedRates.get(level) || 0
+      
+      // 确保 adjustedRate 是数字类型
+      const rate = typeof adjustedRate === 'string' ? parseFloat(adjustedRate) : Number(adjustedRate)
+      
+      if (rate > 0) {
+        const rateDecimal = new Decimal(rate)
+        const commissionAmount = orderPriceDecimal.mul(rateDecimal).div(100).toNumber()
+
+        result.push({
+          level,
+          agentId,
+          rate: rate,  // 使用调整后的比例
+          amount: commissionAmount
+        })
+
+        this.app.log.debug({ 
+          userId, 
+          level, 
+          agentId, 
+          rate, 
+          amount: commissionAmount 
+        }, 'calculateCommission: 添加分润详情')
+      } else {
+        this.app.log.debug({ userId, level, agentId, adjustedRate: rate }, 'calculateCommission: 调整后的比例为 0,跳过该层级')
+      }
+    })
+
+    const totalAmount = result.reduce((sum, item) => sum + item.amount, 0)
+    this.app.log.info({ 
+      userId, 
+      orderPrice, 
+      resultCount: result.length, 
+      totalAmount,
+      details: result.map(item => ({ level: item.level, agentId: item.agentId, rate: item.rate, amount: item.amount }))
+    }, 'calculateCommission: 分润计算完成')
+
+    return result
+  }
+
+  /**
+   * 为 income_records 创建多级分润索引记录
+   * 在创建 income_records 后调用此方法
+   * @param commissionDetails 可选,如果提供则使用已存储的分润信息,否则重新计算
+   */
+  async createCommissionIndex(
+    incomeRecordId: number,
+    orderNo: string,
+    userId: number,
+    orderPrice: number,
+    status: boolean = false,
+    commissionDetails?: CommissionDetail[]
+  ): Promise<void> {
+    this.app.log.debug({ incomeRecordId, orderNo, userId, orderPrice, status, hasStoredDetails: !!commissionDetails }, 'createCommissionIndex: 开始创建分润索引记录')
+    
+    // 如果提供了已存储的分润信息,直接使用;否则重新计算
+    let details: CommissionDetail[]
+    if (commissionDetails && commissionDetails.length > 0) {
+      details = commissionDetails
+      this.app.log.debug({ orderNo, detailsCount: details.length }, 'createCommissionIndex: 使用已存储的分润信息')
+    } else {
+      // 计算多级分润
+      this.app.log.debug({ orderNo, userId, orderPrice }, 'createCommissionIndex: 重新计算多级分润')
+      details = await this.calculateCommission(userId, orderPrice)
+    }
+
+    // 如果没有分润,不创建索引记录
+    if (details.length === 0) {
+      this.app.log.debug({ orderNo, userId }, 'createCommissionIndex: 没有分润详情,跳过创建索引记录')
+      return
+    }
+
+    this.app.log.debug({ 
+      incomeRecordId, 
+      orderNo, 
+      detailsCount: details.length,
+      details: details.map(d => ({ level: d.level, agentId: d.agentId, rate: d.rate, amount: d.amount }))
+    }, 'createCommissionIndex: 准备创建索引记录')
+
+    // 创建索引记录
+    const indexRecords = details.map(detail =>
+      this.indexRepository.create({
+        incomeRecordId,
+        agentId: detail.agentId,
+        level: detail.level,
+        commissionRate: detail.rate,
+        commissionAmount: detail.amount,
+        orderNo,
+        orderPrice,
+        userId,
+        status
+      })
+    )
+
+    await this.indexRepository.save(indexRecords)
+
+    const totalAmount = details.reduce((sum, d) => sum + d.amount, 0)
+    this.app.log.info({ 
+      incomeRecordId, 
+      orderNo, 
+      userId, 
+      levels: details.length, 
+      totalAmount,
+      status,
+      records: indexRecords.map(r => ({ level: r.level, agentId: r.agentId, commissionRate: r.commissionRate, commissionAmount: r.commissionAmount }))
+    }, 'createCommissionIndex: 分润索引记录创建完成')
+  }
+
+  /**
+   * 更新分润索引状态(同步 income_records 的状态)
+   */
+  async updateIndexStatus(incomeRecordId: number, status: boolean): Promise<void> {
+    await this.indexRepository.update(
+      { incomeRecordId },
+      { status }
+    )
+
+    this.app.log.debug(`Updated commission index status for income_record ${incomeRecordId}: ${status}`)
+  }
+
+  /**
+   * 根据订单号更新索引状态
+   */
+  async updateIndexStatusByOrderNo(orderNo: string, status: boolean): Promise<void> {
+    await this.indexRepository.update(
+      { orderNo },
+      { status }
+    )
+
+    this.app.log.debug(`Updated commission index status for order ${orderNo}: ${status}`)
+  }
+
+  /**
+   * 获取代理的总分润(使用索引表,性能最优)
+   */
+  async getAgentTotalCommission(agentId: number): Promise<number> {
+    const result = await this.indexRepository
+      .createQueryBuilder('index')
+      .select('SUM(index.commissionAmount)', 'totalCommission')
+      .where('index.agentId = :agentId', { agentId })
+      .andWhere('index.status = :status', { status: true })
+      .getRawOne()
+
+    return parseFloat(result?.totalCommission || '0')
+  }
+
+  /**
+   * 获取代理的分润详情(包括层级统计)
+   */
+  async getAgentCommissionDetails(agentId: number): Promise<{
+    totalCommission: number
+    orderCount: number
+    totalSales: number
+    avgCommissionRate: number
+    minLevel: number
+    maxLevel: number
+    byLevel: Array<{ level: number; count: number; amount: number }>
+  }> {
+    // 总体统计
+    const summary = await this.indexRepository
+      .createQueryBuilder('index')
+      .select('SUM(index.commissionAmount)', 'totalCommission')
+      .addSelect('COUNT(*)', 'orderCount')
+      .addSelect('SUM(index.orderPrice)', 'totalSales')
+      .addSelect('AVG(index.commissionRate)', 'avgCommissionRate')
+      .addSelect('MIN(index.level)', 'minLevel')
+      .addSelect('MAX(index.level)', 'maxLevel')
+      .where('index.agentId = :agentId', { agentId })
+      .andWhere('index.status = :status', { status: true })
+      .getRawOne()
+
+    // 按层级统计
+    const byLevel = await this.indexRepository
+      .createQueryBuilder('index')
+      .select('index.level', 'level')
+      .addSelect('COUNT(*)', 'count')
+      .addSelect('SUM(index.commissionAmount)', 'amount')
+      .where('index.agentId = :agentId', { agentId })
+      .andWhere('index.status = :status', { status: true })
+      .groupBy('index.level')
+      .orderBy('index.level', 'ASC')
+      .getRawMany()
+
+    return {
+      totalCommission: parseFloat(summary?.totalCommission || '0'),
+      orderCount: parseInt(summary?.orderCount || '0', 10),
+      totalSales: parseFloat(summary?.totalSales || '0'),
+      avgCommissionRate: parseFloat(summary?.avgCommissionRate || '0'),
+      minLevel: parseInt(summary?.minLevel || '0', 10),
+      maxLevel: parseInt(summary?.maxLevel || '0', 10),
+      byLevel: byLevel.map(item => ({
+        level: item.level,
+        count: parseInt(item.count, 10),
+        amount: parseFloat(item.amount)
+      }))
+    }
+  }
+
+  /**
+   * 根据订单号获取分润索引记录
+   */
+  async getIndexByOrderNo(orderNo: string): Promise<AgentCommissionIndex[]> {
+    return await this.indexRepository.find({
+      where: { orderNo },
+      order: { level: 'ASC' }
+    })
+  }
+
+  /**
+   * 根据代理ID获取分润记录列表(使用索引表)
+   */
+  async getRecordsByAgentId(
+    agentId: number,
+    page: number = 0,
+    size: number = 20
+  ): Promise<{
+    content: AgentCommissionIndex[]
+    total: number
+    page: number
+    size: number
+  }> {
+    const [records, total] = await this.indexRepository.findAndCount({
+      where: { agentId, status: true },
+      order: { createdAt: 'DESC' },
+      skip: page * size,
+      take: size
+    })
+
+    return {
+      content: records,
+      total,
+      page,
+      size
+    }
+  }
+
+  /**
+   * 统计代理的总分润(使用索引表)
+   * @param agentId 代理ID
+   * @param startDate 开始日期(可选)
+   * @param endDate 结束日期(可选)
+   */
+  async getAgentTotalCommissionByDateRange(
+    agentId: number,
+    startDate?: Date,
+    endDate?: Date
+  ): Promise<number> {
+    let queryBuilder = this.indexRepository
+      .createQueryBuilder('index')
+      .select('SUM(index.commissionAmount)', 'totalCommission')
+      .where('index.agentId = :agentId', { agentId })
+      .andWhere('index.status = :status', { status: true })
+
+    if (startDate) {
+      queryBuilder = queryBuilder.andWhere('index.createdAt >= :startDate', { startDate })
+    }
+    if (endDate) {
+      queryBuilder = queryBuilder.andWhere('index.createdAt < :endDate', { endDate })
+    }
+
+    const result = await queryBuilder.getRawOne()
+    return parseFloat(result?.totalCommission || '0')
+  }
+
+  /**
+   * 统计多个代理的总分润(使用索引表)
+   * @param agentIds 代理ID数组
+   * @param startDate 开始日期(可选)
+   * @param endDate 结束日期(可选)
+   */
+  async getAgentsTotalCommissionByDateRange(
+    agentIds: number[],
+    startDate?: Date,
+    endDate?: Date
+  ): Promise<Map<number, number>> {
+    if (agentIds.length === 0) {
+      return new Map()
+    }
+
+    let queryBuilder = this.indexRepository
+      .createQueryBuilder('index')
+      .select('index.agentId', 'agentId')
+      .addSelect('SUM(index.commissionAmount)', 'totalCommission')
+      .where('index.agentId IN (:...agentIds)', { agentIds })
+      .andWhere('index.status = :status', { status: true })
+      .groupBy('index.agentId')
+
+    if (startDate) {
+      queryBuilder = queryBuilder.andWhere('index.createdAt >= :startDate', { startDate })
+    }
+    if (endDate) {
+      queryBuilder = queryBuilder.andWhere('index.createdAt < :endDate', { endDate })
+    }
+
+    const results = await queryBuilder.getRawMany()
+    const map = new Map<number, number>()
+    results.forEach(result => {
+      map.set(result.agentId, parseFloat(result.totalCommission || '0'))
+    })
+    return map
+  }
+
+  /**
+   * 根据订单用户ID列表统计总分润(用于域名统计)
+   * @param userIds 订单用户ID数组
+   * @param startDate 开始日期(可选)
+   * @param endDate 结束日期(可选)
+   */
+  async getTotalCommissionByUserIds(
+    userIds: number[],
+    startDate?: Date,
+    endDate?: Date
+  ): Promise<number> {
+    if (userIds.length === 0) {
+      return 0
+    }
+
+    let queryBuilder = this.indexRepository
+      .createQueryBuilder('index')
+      .select('SUM(index.commissionAmount)', 'totalCommission')
+      .where('index.userId IN (:...userIds)', { userIds })
+      .andWhere('index.status = :status', { status: true })
+
+    if (startDate) {
+      queryBuilder = queryBuilder.andWhere('index.createdAt >= :startDate', { startDate })
+    }
+    if (endDate) {
+      queryBuilder = queryBuilder.andWhere('index.createdAt < :endDate', { endDate })
+    }
+
+    const result = await queryBuilder.getRawOne()
+    return parseFloat(result?.totalCommission || '0')
+  }
+}

+ 187 - 0
src/services/page-click-record.service.ts

@@ -0,0 +1,187 @@
+import { FastifyInstance } from 'fastify'
+import Redis from 'ioredis'
+import { PageType } from '../dto/page-click-record.dto'
+
+export class PageClickRecordService {
+  private redis: Redis | null
+  // 支持的页面类型列表
+  private readonly PAGE_TYPES = [PageType.HOME, PageType.VIDEO]
+
+  constructor(app: FastifyInstance) {
+    this.redis = app.redis || null
+  }
+
+  /**
+   * 记录页面点击
+   * 使用Redis计数器,每个页面每天独立计数
+   * @param pageType 页面类型:home 或 video
+   * @param ip 访问IP(可选,用于去重)
+   * @returns 返回当天的点击量
+   */
+  async recordClick(pageType: PageType, ip?: string): Promise<number> {
+    if (!this.redis) {
+      throw new Error('Redis未配置')
+    }
+
+    // 验证页面类型
+    if (!this.PAGE_TYPES.includes(pageType)) {
+      throw new Error(`不支持的页面类型: ${pageType}`)
+    }
+
+    const today = new Date()
+    today.setHours(0, 0, 0, 0)
+    const dateStr = today.toISOString().split('T')[0] // YYYY-MM-DD
+
+    const redisKey = `page:click:${pageType}:${dateStr}`
+
+    // 如果提供了IP,使用Set去重(每个IP每天只计数一次)
+    if (ip) {
+      const setKey = `page:click:ip:${pageType}:${dateStr}`
+      const isNewIp = await this.redis.sadd(setKey, ip)
+      
+      // 如果是新IP,增加点击计数
+      if (isNewIp === 1) {
+        await this.redis.incr(redisKey)
+        // 设置7天过期
+        await this.redis.expire(redisKey, 7 * 24 * 3600)
+      }
+      
+      // 设置IP Set的过期时间
+      await this.redis.expire(setKey, 7 * 24 * 3600)
+      
+      // 获取当前点击量
+      const count = await this.redis.get(redisKey)
+      return parseInt(count || '0', 10)
+    } else {
+      // 没有IP,直接增加计数
+      const count = await this.redis.incr(redisKey)
+      // 设置7天过期
+      await this.redis.expire(redisKey, 7 * 24 * 3600)
+      return count
+    }
+  }
+
+  /**
+   * 获取指定页面的点击量
+   * @param pageType 页面类型
+   * @param date 日期(可选,默认今天)
+   * @returns 点击量
+   */
+  async getClickCount(pageType: PageType, date?: Date): Promise<number> {
+    if (!this.redis) {
+      return 0
+    }
+
+    const targetDate = date || new Date()
+    targetDate.setHours(0, 0, 0, 0)
+    const dateStr = targetDate.toISOString().split('T')[0]
+
+    const redisKey = `page:click:${pageType}:${dateStr}`
+    const count = await this.redis.get(redisKey)
+    return parseInt(count || '0', 10)
+  }
+
+  /**
+   * 获取指定日期范围内页面的点击统计
+   * @param startDate 开始日期
+   * @param endDate 结束日期
+   * @param pageType 可选,指定页面类型,不传则返回所有页面
+   * @returns 统计数据
+   */
+  async getStatistics(
+    startDate: Date,
+    endDate: Date,
+    pageType?: PageType
+  ): Promise<{
+    statistics: Array<{ pageType: PageType; date: string; clickCount: number }>
+    total: Array<{ pageType: PageType; totalClicks: number }>
+  }> {
+    if (!this.redis) {
+      return { statistics: [], total: [] }
+    }
+
+    startDate.setHours(0, 0, 0, 0)
+    endDate.setHours(23, 59, 59, 999)
+
+    const statistics: Array<{ pageType: PageType; date: string; clickCount: number }> = []
+    const totalMap = new Map<PageType, number>()
+
+    // 生成日期范围
+    const dates: string[] = []
+    const currentDate = new Date(startDate)
+    while (currentDate <= endDate) {
+      dates.push(currentDate.toISOString().split('T')[0])
+      currentDate.setDate(currentDate.getDate() + 1)
+    }
+
+    // 确定要查询的页面类型列表
+    const pageTypesToQuery = pageType ? [pageType] : this.PAGE_TYPES
+
+    // 查询每个页面的统计数据
+    for (const type of pageTypesToQuery) {
+      for (const dateStr of dates) {
+        const redisKey = `page:click:${type}:${dateStr}`
+        const count = await this.redis.get(redisKey)
+        const clickCount = parseInt(count || '0', 10)
+        
+        if (clickCount > 0) {
+          statistics.push({
+            pageType: type,
+            date: dateStr,
+            clickCount
+          })
+          
+          // 累加总数
+          const currentTotal = totalMap.get(type) || 0
+          totalMap.set(type, currentTotal + clickCount)
+        }
+      }
+    }
+
+    // 转换为数组格式
+    const total = Array.from(totalMap.entries()).map(([pageType, totalClicks]) => ({
+      pageType,
+      totalClicks
+    }))
+
+    // 按日期和页面类型排序
+    statistics.sort((a, b) => {
+      if (a.date !== b.date) {
+        return a.date.localeCompare(b.date)
+      }
+      return a.pageType.localeCompare(b.pageType)
+    })
+
+    return { statistics, total }
+  }
+
+  /**
+   * 获取所有页面的今日点击统计汇总
+   * @returns 所有页面的今日点击量
+   */
+  async getTodaySummary(): Promise<Array<{ pageType: PageType; clickCount: number }>> {
+    if (!this.redis) {
+      return []
+    }
+
+    const today = new Date()
+    today.setHours(0, 0, 0, 0)
+    const dateStr = today.toISOString().split('T')[0]
+
+    // 查询每个页面的今日点击量
+    const summary: Array<{ pageType: PageType; clickCount: number }> = []
+    for (const pageType of this.PAGE_TYPES) {
+      const redisKey = `page:click:${pageType}:${dateStr}`
+      const count = await this.redis.get(redisKey)
+      const clickCount = parseInt(count || '0', 10)
+      
+      summary.push({ pageType, clickCount })
+    }
+
+    // 按点击量降序排序
+    summary.sort((a, b) => b.clickCount - a.clickCount)
+
+    return summary
+  }
+}
+

+ 106 - 139
src/services/payment.service.ts

@@ -26,6 +26,7 @@ import { UserService } from './user.service'
 import { TeamService } from './team.service'
 import { SysConfigService } from './sys-config.service'
 import { VipLevel } from '../entities/member.entity'
+import { MultiLevelCommissionService } from './multi-level-commission.service'
 
 export class PaymentService {
   private app: FastifyInstance
@@ -39,6 +40,7 @@ export class PaymentService {
   private sysConfigService: SysConfigService
   private teamDomainService: TeamDomainService
   private teamMembersService: TeamMembersService
+  private multiLevelCommissionService: MultiLevelCommissionService
 
   private static readonly ORDER_TYPE_TO_VIP_LEVEL_MAP: Record<OrderType, VipLevel> = {
     [OrderType.SINGLE_TIP]: VipLevel.FREE,
@@ -74,6 +76,7 @@ export class PaymentService {
     this.sysConfigService = new SysConfigService(app)
     this.teamDomainService = new TeamDomainService(app)
     this.teamMembersService = new TeamMembersService(app)
+    this.multiLevelCommissionService = new MultiLevelCommissionService(app)
   }
 
   private generateSign(params: Record<string, any>, key: string): string {
@@ -208,7 +211,7 @@ export class PaymentService {
       }
     } catch (userError) {
       // 获取user信息失败,继续使用默认逻辑
-      this.app.log.warn(`Failed to get user info for member ${member.id}, using default commission`, userError)
+      this.app.log.warn({ err: userError, memberId: member.id }, 'Failed to get user info for member, using default commission')
     }
     
     // 默认只给团队分成
@@ -351,6 +354,32 @@ export class PaymentService {
       })
       this.app.log.info('Income record status updated successfully')
 
+      // 支付成功后创建多级分润索引表(使用已存储的分润信息)
+      try {
+        // 从已存储的分润信息创建索引表
+        let commissionDetails: any[] | undefined
+        if (incomeRecord.commissionDetails) {
+          try {
+            commissionDetails = JSON.parse(incomeRecord.commissionDetails)
+          } catch (parseError) {
+            this.app.log.warn({ err: parseError }, 'Failed to parse stored commission details, will recalculate')
+          }
+        }
+
+        await this.multiLevelCommissionService.createCommissionIndex(
+          incomeRecord.id,
+          params.out_trade_no,
+          incomeRecord.userId,
+          incomeRecord.orderPrice,
+          true, // 支付成功,状态为已支付
+          commissionDetails // 使用已存储的分润信息,如果解析失败则重新计算
+        )
+        this.app.log.info('Commission index created successfully after payment')
+      } catch (indexError) {
+        this.app.log.error({ err: indexError }, 'Failed to create commission index after payment')
+        // 不影响主流程,继续执行
+      }
+
       // 更新会员等级
       const vipLevel = this.getVipLevelByOrderType(incomeRecord.orderType)
 
@@ -401,87 +430,56 @@ export class PaymentService {
       }
       const result = await this.createOrder(paymentParams)
 
-      // 创建收入记录
+      // 创建收入记录(计算并存储分润信息)
       try {
-        // 获取member信息,如果失败则使用原有逻辑
-        let commissionInfo = { 
-          teamAgentId: 0, 
-          teamCommissionRate: 0, 
-          personalAgentId: 0, 
-          personalCommissionRate: 0, 
-          isPersonalCommission: false 
-        }
-        
+        // 获取 member 信息,用于分润计算(优先使用 domain 绑定关系)
+        const member = await this.memberService.findByUserId(user.id)
+
+        // 计算到 team 层的分润金额(用于 incomeAmount 统计)
+        let teamCommission = { agentId: 0, commissionRate: 0, amount: 0 }
         try {
-          const member = await this.memberService.findByUserId(user.id)
-          if (member) {
-            // 使用新的分成逻辑
-            commissionInfo = await this.getCommissionInfo(member)
-          } else {
-            // 如果没有member信息,回退到原有的团队分成逻辑
-            this.app.log.warn(`Member not found for user ${user.id}, using fallback commission logic`)
-            const fallbackCommission = await this.getFallbackCommission(user)
-            commissionInfo = {
-              teamAgentId: fallbackCommission.agentId,
-              teamCommissionRate: fallbackCommission.commissionRate,
-              personalAgentId: 0,
-              personalCommissionRate: 0,
-              isPersonalCommission: false
-            }
-          }
-        } catch (memberError) {
-          // 获取member信息失败,使用原有逻辑
-          this.app.log.warn(
-            { err: memberError, userId: user.id },
-            `Failed to get member info for user ${user.id}, using fallback commission logic`
+          const firstTeam = await this.multiLevelCommissionService.getFirstTeamCommission(
+            user.id,
+            new Decimal(price).toNumber(),
+            member || null
           )
-          const fallbackCommission = await this.getFallbackCommission(user)
-          commissionInfo = {
-            teamAgentId: fallbackCommission.agentId,
-            teamCommissionRate: fallbackCommission.commissionRate,
-            personalAgentId: 0,
-            personalCommissionRate: 0,
-            isPersonalCommission: false
-          }
-        }
-
-        // 计算个人分成金额
-        let personalIncomeAmount = new Decimal(0)
-        if (commissionInfo.personalAgentId > 0 && commissionInfo.personalCommissionRate > 0) {
-          const commissionRate = new Decimal(commissionInfo.personalCommissionRate)
-          const hundred = new Decimal(100)
-          personalIncomeAmount = new Decimal(price).mul(commissionRate).div(hundred)
-        }
-
-        // 计算团队分成金额
-        let teamIncomeAmount = new Decimal(0)
-        if (commissionInfo.teamAgentId > 0 && commissionInfo.teamCommissionRate > 0) {
-          const commissionRate = new Decimal(commissionInfo.teamCommissionRate)
-          const hundred = new Decimal(100)
-          const totalTeamIncome = new Decimal(price).mul(commissionRate).div(hundred)
-          
-          // 如果有个人分成,团队实际分到的金额 = 团队总分成 - 个人分成
-          if (commissionInfo.isPersonalCommission && personalIncomeAmount.greaterThan(0)) {
-            teamIncomeAmount = totalTeamIncome.sub(personalIncomeAmount)
-          } else {
-            teamIncomeAmount = totalTeamIncome
+          if (firstTeam) {
+            teamCommission = firstTeam
           }
+        } catch (teamError) {
+          this.app.log.warn({ err: teamError }, 'Failed to calculate team commission, using default')
         }
 
-        // 创建收入记录,包含团队和个人分成信息
-        await this.incomeRecordsService.create({
-          agentId: commissionInfo.teamAgentId,
+        // 计算多级分润并存储
+        const orderPriceNum = new Decimal(price).toNumber()
+        const commissionDetails = await this.multiLevelCommissionService.calculateCommission(
+          user.id,
+          orderPriceNum,
+          member || null
+        )
+        const commissionDetailsJson = JSON.stringify(commissionDetails)
+
+        // 计算所有层级的总分润金额
+        const totalCommissionAmount = commissionDetails.reduce((sum, detail) => {
+          return sum + detail.amount
+        }, 0)
+
+        const incomeRecord = await this.incomeRecordsService.create({
+          agentId: teamCommission.agentId, // team 层的 agentId(团队信息)
           userId: user.id,
-          personalAgentId: commissionInfo.personalAgentId,
-          personalIncomeAmount: personalIncomeAmount.toNumber(),
-          incomeAmount: teamIncomeAmount.toNumber(),
+          personalAgentId: 0,
+          personalIncomeAmount: 0,
+          incomeAmount: totalCommissionAmount, // 所有层级的总分润金额
           incomeType: IncomeType.COMMISSION,
           orderType: this.getOrderTypeByType(params.type),
-          orderPrice: new Decimal(price).toNumber(),
+          orderPrice: orderPriceNum,
           orderNo: out_trade_no,
           payChannel: 'alipay',
-          payNo: result.trade_no || ''
+          payNo: result.trade_no || '',
+          commissionDetails: commissionDetailsJson // 存储分润信息
         })
+
+        // 索引表在支付成功后再创建,使用已存储的分润信息
       } catch (incomeError) {
         this.app.log.error({ err: incomeError }, 'Failed to create income record')
         throw new Error('操作失败')
@@ -532,88 +530,57 @@ export class PaymentService {
 
       const result = await this.createOrder(paymentParams)
 
-      // 创建收入记录
+      // 创建收入记录(计算并存储分润信息)
       try {
-        // 获取member信息,如果失败则使用原有逻辑
-        let commissionInfo = { 
-          teamAgentId: 0, 
-          teamCommissionRate: 0, 
-          personalAgentId: 0, 
-          personalCommissionRate: 0, 
-          isPersonalCommission: false 
-        }
-        
+        // 获取 member 信息,用于分润计算(优先使用 domain 绑定关系)
+        const member = await this.memberService.findByUserId(user.id)
+
+        // 计算到 team 层的分润金额(用于 incomeAmount 统计)
+        let teamCommission = { agentId: 0, commissionRate: 0, amount: 0 }
         try {
-          const member = await this.memberService.findByUserId(user.id)
-          if (member) {
-            // 使用新的分成逻辑
-            commissionInfo = await this.getCommissionInfo(member)
-          } else {
-            // 如果没有member信息,回退到原有的团队分成逻辑
-            this.app.log.warn(`Member not found for user ${user.id}, using fallback commission logic`)
-            const fallbackCommission = await this.getFallbackCommission(user)
-            commissionInfo = {
-              teamAgentId: fallbackCommission.agentId,
-              teamCommissionRate: fallbackCommission.commissionRate,
-              personalAgentId: 0,
-              personalCommissionRate: 0,
-              isPersonalCommission: false
-            }
-          }
-        } catch (memberError) {
-          // 获取member信息失败,使用原有逻辑
-          this.app.log.warn(
-            { err: memberError, userId: user.id },
-            `Failed to get member info for user ${user.id}, using fallback commission logic`
+          const firstTeam = await this.multiLevelCommissionService.getFirstTeamCommission(
+            user.id,
+            new Decimal(price).toNumber(),
+            member || null
           )
-          const fallbackCommission = await this.getFallbackCommission(user)
-          commissionInfo = {
-            teamAgentId: fallbackCommission.agentId,
-            teamCommissionRate: fallbackCommission.commissionRate,
-            personalAgentId: 0,
-            personalCommissionRate: 0,
-            isPersonalCommission: false
+          if (firstTeam) {
+            teamCommission = firstTeam
           }
+        } catch (teamError) {
+          this.app.log.warn({ err: teamError }, 'Failed to calculate team commission, using default')
         }
 
-        // 计算个人分成金额
-        let personalIncomeAmount = new Decimal(0)
-        if (commissionInfo.personalAgentId > 0 && commissionInfo.personalCommissionRate > 0) {
-          const commissionRate = new Decimal(commissionInfo.personalCommissionRate)
-          const hundred = new Decimal(100)
-          personalIncomeAmount = new Decimal(price).mul(commissionRate).div(hundred)
-        }
-
-        // 计算团队分成金额
-        let teamIncomeAmount = new Decimal(0)
-        if (commissionInfo.teamAgentId > 0 && commissionInfo.teamCommissionRate > 0) {
-          const commissionRate = new Decimal(commissionInfo.teamCommissionRate)
-          const hundred = new Decimal(100)
-          const totalTeamIncome = new Decimal(price).mul(commissionRate).div(hundred)
-          
-          // 如果有个人分成,团队实际分到的金额 = 团队总分成 - 个人分成
-          if (commissionInfo.isPersonalCommission && personalIncomeAmount.greaterThan(0)) {
-            teamIncomeAmount = totalTeamIncome.sub(personalIncomeAmount)
-          } else {
-            teamIncomeAmount = totalTeamIncome
-          }
-        }
-
-        // 创建收入记录,包含团队和个人分成信息
-        await this.incomeRecordsService.create({
-          agentId: commissionInfo.teamAgentId,
+        // 计算多级分润并存储
+        const orderPriceNum = new Decimal(price).toNumber()
+        const commissionDetails = await this.multiLevelCommissionService.calculateCommission(
+          user.id,
+          orderPriceNum,
+          member || null
+        )
+        const commissionDetailsJson = JSON.stringify(commissionDetails)
+
+        // 计算所有层级的总分润金额
+        const totalCommissionAmount = commissionDetails.reduce((sum, detail) => {
+          return sum + detail.amount
+        }, 0)
+
+        const incomeRecord = await this.incomeRecordsService.create({
+          agentId: teamCommission.agentId, // team 层的 agentId(团队信息)
           userId: user.id,
-          personalAgentId: commissionInfo.personalAgentId,
-          personalIncomeAmount: personalIncomeAmount.toNumber(),
-          incomeAmount: teamIncomeAmount.toNumber(),
+          personalAgentId: 0,
+          personalIncomeAmount: 0,
+          incomeAmount: totalCommissionAmount, // 所有层级的总分润金额
           incomeType: IncomeType.TIP,
           orderType: OrderType.SINGLE_TIP,
-          orderPrice: new Decimal(price).toNumber(),
+          orderPrice: orderPriceNum,
           orderNo: out_trade_no,
           payChannel: 'alipay',
           payNo: result.trade_no || '',
-          resourceId: params.resourceId.toString()
+          resourceId: params.resourceId.toString(),
+          commissionDetails: commissionDetailsJson // 存储分润信息
         })
+
+        // 索引表在支付成功后再创建,使用已存储的分润信息
       } catch (incomeError) {
         this.app.log.error({ err: incomeError }, 'Failed to create income record')
         throw new Error('Failed to create income record')

+ 80 - 19
src/services/sys-config.service.ts

@@ -12,6 +12,7 @@ import {
 import { User, UserRole } from '../entities/user.entity'
 import { Team } from '../entities/team.entity'
 import { TeamMembers } from '../entities/team-members.entity'
+import { Member } from '../entities/member.entity'
 
 export class SysConfigService {
   private app: FastifyInstance
@@ -19,6 +20,7 @@ export class SysConfigService {
   private userRepository: Repository<User>
   private teamRepository: Repository<Team>
   private teamMembersRepository: Repository<TeamMembers>
+  private memberRepository: Repository<Member>
 
   constructor(app: FastifyInstance) {
     this.app = app
@@ -26,6 +28,7 @@ export class SysConfigService {
     this.userRepository = app.dataSource.getRepository(User)
     this.teamRepository = app.dataSource.getRepository(Team)
     this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
+    this.memberRepository = app.dataSource.getRepository(Member)
   }
 
   async getSysConfig(name: string, teamId?: number) {
@@ -112,30 +115,32 @@ export class SysConfigService {
   }
 
   async list(page: number = 0, size: number = 20, name?: string, type?: ConfigType, teamId?: number) {
-    const where: any = {
-      name: Not('sensitive_words')
-    }
+    const queryBuilder = this.sysConfigRepository.createQueryBuilder('config')
+      .where('config.name != :excludedName', { excludedName: 'sensitive_words' })
 
     if (name) {
-      where.name = Like(`%${name}%`)
+      queryBuilder.andWhere('config.name LIKE :name', { name: `%${name}%` })
     }
 
     if (type) {
-      where.type = type
+      queryBuilder.andWhere('config.type = :type', { type })
     }
 
     if (teamId !== undefined) {
-      where.teamId = teamId
+      queryBuilder.andWhere('config.teamId = :teamId', { teamId })
     }
 
-    const [data, total] = await this.sysConfigRepository.findAndCount({
-      where,
-      skip: page * size,
-      take: size,
-      order: {
-        id: 'ASC'
-      }
-    })
+    // 排序:teamId为0的展示在最上方,其他的按照teamId排序
+    queryBuilder
+      .orderBy('CASE WHEN config.teamId = 0 THEN 0 ELSE 1 END', 'ASC')
+      .addOrderBy('config.teamId', 'ASC')
+      .addOrderBy('config.id', 'ASC')
+
+    const total = await queryBuilder.getCount()
+    const data = await queryBuilder
+      .skip(page * size)
+      .take(size)
+      .getMany()
 
     return {
       data,
@@ -262,14 +267,70 @@ export class SysConfigService {
     }
   }
 
+  /**
+   * 从 parentId 递归查找 team 用户,直到找到 team 或到达顶层
+   * @param parentId 父用户ID
+   * @param maxDepth 最大递归深度,防止无限循环
+   * @returns teamId,如果找不到则返回 0
+   */
+  private async findTeamIdByParentId(parentId: number, maxDepth: number = 10): Promise<number> {
+    if (maxDepth <= 0 || !parentId || parentId <= 0) {
+      return 0
+    }
+
+    try {
+      const parentUser = await this.userRepository.findOne({ where: { id: parentId } })
+      if (!parentUser) {
+        return 0
+      }
+
+      // 如果父用户是 team 用户,直接查找对应的 team
+      if (parentUser.role === UserRole.TEAM) {
+        const team = await this.teamRepository.findOne({ where: { userId: parentId } })
+        if (team) {
+          return team.id
+        }
+      }
+
+      // 如果父用户是推广员(promoter),可能是二级代理,继续向上查找
+      if (parentUser.role === UserRole.PROMOTER && parentUser.parentId && parentUser.parentId > 0) {
+        return await this.findTeamIdByParentId(parentUser.parentId, maxDepth - 1)
+      }
+
+      // 如果父用户有 parentId,继续向上查找
+      if (parentUser.parentId && parentUser.parentId > 0) {
+        return await this.findTeamIdByParentId(parentUser.parentId, maxDepth - 1)
+      }
+    } catch (error) {
+      this.app.log.warn({ err: error, parentId }, 'Failed to find team by parentId')
+    }
+
+    return 0
+  }
+
   async getUserTeamConfigs(userId?: number) {
     let teamId = 0
+    
     if (userId) {
-      const user = await this.userRepository.findOne({ where: { id: userId } })
-      if (user) {
-        const team = await this.teamRepository.findOne({ where: { userId: user.parentId } })
-        if (team) {
-          teamId = team.id
+      // 方案1:优先从 member 表读取 teamId
+      try {
+        const member = await this.memberRepository.findOne({ where: { userId } })
+        if (member && member.teamId && member.teamId > 0) {
+          teamId = member.teamId
+        }
+      } catch (error) {
+        this.app.log.warn({ err: error, userId }, 'Failed to get teamId from member table')
+      }
+
+      // 方案2:如果 member 表中没有有效的 teamId,从 parentId 递归查找 team
+      if (teamId === 0) {
+        try {
+          const user = await this.userRepository.findOne({ where: { id: userId } })
+          if (user && user.parentId && user.parentId > 0) {
+            teamId = await this.findTeamIdByParentId(user.parentId)
+          }
+        } catch (error) {
+          this.app.log.warn({ err: error, userId }, 'Failed to get teamId from parentId')
         }
       }
     }

+ 13 - 10
src/services/team-domain.service.ts

@@ -1,4 +1,4 @@
-import { Repository, Like, Between } from 'typeorm'
+import { Repository, Like, Between, In } from 'typeorm'
 import { FastifyInstance } from 'fastify'
 import { TeamDomain } from '../entities/team-domain.entity'
 import { TeamMembers } from '../entities/team-members.entity'
@@ -8,6 +8,7 @@ import { UserService } from './user.service'
 import { TeamService } from './team.service'
 import { Member } from '../entities/member.entity'
 import { IncomeRecords } from '../entities/income-records.entity'
+import { MultiLevelCommissionService } from './multi-level-commission.service'
 
 export class TeamDomainService {
   private teamDomainRepository: Repository<TeamDomain>
@@ -16,6 +17,7 @@ export class TeamDomainService {
   private userService: UserService
   private teamService: TeamService
   private teamMembersRepository: Repository<TeamMembers>
+  private multiLevelCommissionService: MultiLevelCommissionService
   constructor(app: FastifyInstance) {
     this.teamDomainRepository = app.dataSource.getRepository(TeamDomain)
     this.memberRepository = app.dataSource.getRepository(Member)
@@ -23,6 +25,7 @@ export class TeamDomainService {
     this.userService = new UserService(app)
     this.teamService = new TeamService(app)
     this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
+    this.multiLevelCommissionService = new MultiLevelCommissionService(app)
   }
 
   async create(data: CreateTeamDomainBody): Promise<TeamDomain> {
@@ -72,7 +75,7 @@ export class TeamDomainService {
 
     // 检查已存在的域名
     const existingDomains = await this.teamDomainRepository.find({
-      where: { domain: domains.map(d => ({ domain: d })) as any }
+      where: { domain: In(domains) }
     })
     const existingDomainSet = new Set(existingDomains.map(d => d.domain))
 
@@ -283,11 +286,11 @@ export class TeamDomainService {
         }
       })
 
-      // 统计今日收入(包括团队分成和个人分成
+      // 统计今日收入(使用 incomeAmount,统计团队分润
       const todayIncomeRecords = await this.incomeRecordsRepository
         .createQueryBuilder('record')
         .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.incomeAmount + record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
+        .select('SUM(CAST(record.incomeAmount AS DECIMAL(10,5)))', 'totalIncome')
         .where('m.domainId = :domainId', { domainId: teamDomain.id })
         .andWhere('record.createdAt >= :startDate', { startDate: today })
         .andWhere('record.createdAt < :endDate', { endDate: tomorrow })
@@ -297,11 +300,11 @@ export class TeamDomainService {
 
       const todayIncome = todayIncomeRecords?.totalIncome ? parseFloat(todayIncomeRecords.totalIncome) : 0
 
-      // 统计历史总收入(包括团队分成和个人分成
+      // 统计历史总收入(使用 incomeAmount,统计团队分润
       const totalIncomeRecords = await this.incomeRecordsRepository
         .createQueryBuilder('record')
         .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.incomeAmount + record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
+        .select('SUM(CAST(record.incomeAmount AS DECIMAL(10,5)))', 'totalIncome')
         .where('m.domainId = :domainId', { domainId: teamDomain.id })
         .andWhere('record.delFlag = :delFlag', { delFlag: false })
         .andWhere('record.status = :status', { status: true })
@@ -386,11 +389,11 @@ export class TeamDomainService {
         }
       })
 
-      // 统计总收入(包括团队分成和个人分成
+      // 统计总收入(使用 incomeAmount,统计团队分润
       const totalIncomeRecords = await this.incomeRecordsRepository
         .createQueryBuilder('record')
         .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.incomeAmount + record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
+        .select('SUM(CAST(record.incomeAmount AS DECIMAL(10,5)))', 'totalIncome')
         .where('m.domainId = :domainId', { domainId: teamDomain.id })
         .andWhere('record.delFlag = :delFlag', { delFlag: false })
         .andWhere('record.status = :status', { status: true })
@@ -398,11 +401,11 @@ export class TeamDomainService {
 
       const totalIncome = totalIncomeRecords?.totalIncome ? parseFloat(totalIncomeRecords.totalIncome) : 0
 
-      // 统计今日收入(包括团队分成和个人分成
+      // 统计今日收入(使用 incomeAmount,统计团队分润
       const todayIncomeRecords = await this.incomeRecordsRepository
         .createQueryBuilder('record')
         .innerJoin('member', 'm', 'm.userId = record.userId')
-        .select('SUM(CAST(record.incomeAmount + record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
+        .select('SUM(CAST(record.incomeAmount AS DECIMAL(10,5)))', 'totalIncome')
         .where('m.domainId = :domainId', { domainId: teamDomain.id })
         .andWhere('record.createdAt >= :startDate', { startDate: today })
         .andWhere('record.createdAt < :endDate', { endDate: tomorrow })

+ 634 - 142
src/services/team-members.service.ts

@@ -1,18 +1,21 @@
-import { Repository, Like, Not } from 'typeorm'
+import { Repository, Like, Not, In } from 'typeorm'
 import { FastifyInstance } from 'fastify'
 import { TeamMembers } from '../entities/team-members.entity'
 import { IncomeRecords } from '../entities/income-records.entity'
 import { Member } from '../entities/member.entity'
 import { TeamDomain } from '../entities/team-domain.entity'
 import { Team } from '../entities/team.entity'
+import { User } from '../entities/user.entity'
+import { AgentCommissionIndex } from '../entities/agent-commission-index.entity'
 import { PaginationResponse } from '../dto/common.dto'
-import { CreateTeamMembersBody, UpdateTeamMembersBody, ListTeamMembersQuery, TeamMemberStatsQuery, TeamMemberStatsResponse, TeamLeaderStatsQuery, TeamLeaderStatsResponse } from '../dto/team-members.dto'
+import { CreateTeamMembersBody, UpdateTeamMembersBody, ListTeamMembersQuery, TeamMemberStatsQuery, TeamMemberStatsResponse, TeamLeaderStatsQuery, TeamLeaderStatsResponse, TeamMemberTreeNode, TeamMemberStatsTreeNode } from '../dto/team-members.dto'
 import { UserService } from './user.service'
 import { UserRole } from '../entities/user.entity'
 import * as randomstring from 'randomstring'
 import { SysConfigService } from './sys-config.service'
 import { PromotionLinkService } from './promotion-link.service'
 import { LinkType } from '../entities/promotion-link.entity'
+import { MultiLevelCommissionService } from './multi-level-commission.service'
 
 export class TeamMembersService {
   private teamMembersRepository: Repository<TeamMembers>
@@ -21,8 +24,11 @@ export class TeamMembersService {
   private userService: UserService
   private teamDomainRepository: Repository<TeamDomain>
   private teamRepository: Repository<Team>
+  private userRepository: Repository<User>
+  private indexRepository: Repository<AgentCommissionIndex>
   private sysConfigService: SysConfigService
   private promotionLinkService: PromotionLinkService
+  private multiLevelCommissionService: MultiLevelCommissionService
   private app: FastifyInstance
 
   constructor(app: FastifyInstance) {
@@ -33,8 +39,11 @@ export class TeamMembersService {
     this.userService = new UserService(app)
     this.teamDomainRepository = app.dataSource.getRepository(TeamDomain)
     this.teamRepository = app.dataSource.getRepository(Team)
+    this.userRepository = app.dataSource.getRepository(User)
+    this.indexRepository = app.dataSource.getRepository(AgentCommissionIndex)
     this.sysConfigService = new SysConfigService(app)
     this.promotionLinkService = new PromotionLinkService(app)
+    this.multiLevelCommissionService = new MultiLevelCommissionService(app)
   }
 
   /**
@@ -42,8 +51,14 @@ export class TeamMembersService {
    * @param teamId 团队ID
    * @param commissionRate 个人分成比例
    * @param excludeMemberId 排除的成员ID(用于更新时排除自己)
+   * @param parentMemberId 父级团队成员ID(如果有父级,则验证不能大于父级比例)
    */
-  private async validateCommissionRate(teamId: number, commissionRate: number, excludeMemberId?: number): Promise<void> {
+  private async validateCommissionRate(
+    teamId: number, 
+    commissionRate: number, 
+    excludeMemberId?: number,
+    parentMemberId?: number | null
+  ): Promise<void> {
     if (commissionRate <= 0) {
       return // 0或负数不需要验证
     }
@@ -63,24 +78,81 @@ export class TeamMembersService {
     if (commissionRate > teamCommissionRate) {
       throw new Error(`个人分成比例(${commissionRate}%)不能高于团队分成比例(${teamCommissionRate}%)`)
     }
+
+    // 如果有父级团队成员,验证不能大于父级的分成比例
+    if (parentMemberId !== undefined && parentMemberId !== null) {
+      const parentMember = await this.teamMembersRepository.findOne({ where: { id: parentMemberId } })
+      if (parentMember) {
+        const parentCommissionRate = Number(parentMember.commissionRate)
+        if (parentCommissionRate > 0 && commissionRate > parentCommissionRate) {
+          throw new Error(`个人分成比例(${commissionRate}%)不能高于上级分成比例(${parentCommissionRate}%)`)
+        }
+      }
+    }
   }
 
   async create(data: CreateTeamMembersBody, creatorId: number): Promise<TeamMembers> {
-    const { password, teamUserId, ...teamMemberData } = data
+    const { password, teamUserId, memberId, ...teamMemberData } = data
 
-    // 验证分成比例
-    if (teamMemberData.commissionRate !== undefined && teamMemberData.commissionRate > 0) {
-      await this.validateCommissionRate(teamMemberData.teamId, teamMemberData.commissionRate)
+    // 获取团队信息
+    const team = await this.teamRepository.findOne({ where: { id: teamMemberData.teamId } })
+    if (!team) {
+      throw new Error('团队不存在')
     }
 
-    const existingUser = await this.userService.findByName(teamMemberData.name)
-    if (existingUser) {
+    let createdUser: User
+    let parentId: number
+    let parentTeamMember: TeamMembers | null = null
+
+    // 如果提供了 memberId,则该 memberId 是 team_members 表的 id(现有团队成员的 id)
+    // 新创建的团队成员作为该现有团队成员的下级
+    if (memberId) {
+      // 查询父级团队成员信息(team_members 表)
+      parentTeamMember = await this.teamMembersRepository.findOne({ where: { id: memberId } })
+      if (!parentTeamMember) {
+        throw new Error('父级团队成员不存在')
+      }
+
+      // 获取父级团队成员对应的用户
+      const parentTeamMemberUser = await this.userRepository.findOne({ where: { id: parentTeamMember.userId } })
+      if (!parentTeamMemberUser) {
+        throw new Error('父级团队成员对应的用户不存在')
+      }
+
+      // 检查用户名是否已存在
+      const existingUser = await this.userService.findByName(teamMemberData.name)
+      if (existingUser) {
         throw new Error('操作失败')
+      }
+
+      // 如果用户提供了密码且密码不为空,使用用户提供的密码;否则使用默认密码
+      const userPassword = (password && password.trim() !== '') ? password : 'password123'
+      // 新成员的 parentId 设置为父级团队成员的 userId
+      parentId = parentTeamMemberUser.id
+      createdUser = await this.userService.create(userPassword, teamMemberData.name, UserRole.PROMOTER, parentId)
+    } else {
+      // 没有提供 memberId,创建新用户作为团队下的二级代理
+      const existingUser = await this.userService.findByName(teamMemberData.name)
+      if (existingUser) {
+        throw new Error('操作失败')
+      }
+
+      // 如果用户提供了密码且密码不为空,使用用户提供的密码;否则使用默认密码
+      const userPassword = (password && password.trim() !== '') ? password : 'password123'
+      // 二级代理的 parentId 设置为团队用户ID
+      parentId = teamUserId || team.userId
+      createdUser = await this.userService.create(userPassword, teamMemberData.name, UserRole.PROMOTER, parentId)
     }
 
-    const userPassword = password || 'password123'
-    const parentId = teamUserId || creatorId
-    const createdUser = await this.userService.create(userPassword, teamMemberData.name, UserRole.PROMOTER, parentId)
+    // 验证分成比例(如果有父级,需要验证不能大于父级比例)
+    if (teamMemberData.commissionRate !== undefined && teamMemberData.commissionRate > 0) {
+      await this.validateCommissionRate(
+        teamMemberData.teamId, 
+        teamMemberData.commissionRate, 
+        undefined,
+        parentTeamMember ? parentTeamMember.id : null
+      )
+    }
 
     // 生成推广码
     const randomSuffix = randomstring.generate({
@@ -94,11 +166,13 @@ export class TeamMembersService {
       finalPromoCode = `${randomSuffix}${counter}`
     }
 
-    const teamMember = this.teamMembersRepository.create({
+    const teamMemberDataWithParent = {
       ...teamMemberData,
       userId: createdUser.id,
-      promoCode: finalPromoCode
-    })
+      promoCode: finalPromoCode,
+      parentId: memberId || undefined // 如果提供了 memberId,设置为父级团队成员的 id;否则为 undefined(二级代理)
+    }
+    const teamMember = this.teamMembersRepository.create(teamMemberDataWithParent)
     return this.teamMembersRepository.save(teamMember)
   }
 
@@ -106,55 +180,358 @@ export class TeamMembersService {
     return this.teamMembersRepository.findOneOrFail({ where: { id } })
   }
 
-  async findAll(query: ListTeamMembersQuery): Promise<PaginationResponse<TeamMembers>> {
+  /**
+   * 获取团队成员列表(树状结构)
+   * 返回树状结构:团队 -> 团队下级代理 -> 其他下级代理
+   */
+  async findAll(query: ListTeamMembersQuery): Promise<PaginationResponse<TeamMemberTreeNode>> {
     const { page, size, name, teamId, userId } = query
 
+    // 构建查询条件
     const where: any = {}
-
     if (name) {
       where.name = Like(`%${name}%`)
     }
-
     if (teamId) {
       where.teamId = teamId
     }
-
     if (userId) {
       where.userId = userId
     }
 
-    const [members, total] = await this.teamMembersRepository.findAndCount({
+    // 获取所有团队成员(不分页,用于构建树)
+    const allMembers = await this.teamMembersRepository.find({
       where,
-      skip: (Number(page) || 0) * (Number(size) || 20),
-      take: Number(size) || 20,
       order: { createdAt: 'DESC' }
     })
 
+    // 获取团队信息(如果指定了 teamId)
+    let team: Team | null = null
+    if (teamId) {
+      team = await this.teamRepository.findOne({ where: { id: teamId } })
+    }
+
+    // 构建树状结构
+    const tree = await this.buildTeamMemberTree(allMembers, team)
+
+    // 应用分页(对树的第一层进行分页)
+    const pageNum = Number(page) || 0
+    const sizeNum = Number(size) || 20
+    const start = pageNum * sizeNum
+    const end = start + sizeNum
+    const paginatedTree = tree.slice(start, end)
+
     return {
-      content: members,
+      content: paginatedTree,
       metadata: {
-        total: Number(total),
-        page: Number(page) || 0,
-        size: Number(size) || 20
+        total: tree.length,
+        page: pageNum,
+        size: sizeNum
       }
     }
   }
 
+  /**
+   * 构建团队成员树状结构
+   * 结构:基于 team_members.parentId 构建树状层级关系
+   */
+  private async buildTeamMemberTree(
+    members: TeamMembers[],
+    team: Team | null
+  ): Promise<TeamMemberTreeNode[]> {
+    // 如果没有团队信息,基于 parentId 构建树状结构
+    if (!team) {
+      // 构建成员映射(id -> TeamMembers)
+      const memberMap = new Map<number, TeamMembers>()
+      members.forEach(member => {
+        memberMap.set(member.id, member)
+      })
+
+      // 构建父子关系映射(parentId -> TeamMembers[])
+      const childrenMap = new Map<number | null, TeamMembers[]>()
+      members.forEach(member => {
+        const parentId = member.parentId
+        if (!childrenMap.has(parentId)) {
+          childrenMap.set(parentId, [])
+        }
+        childrenMap.get(parentId)!.push(member)
+      })
+
+      // 递归构建树节点
+      const buildNode = (member: TeamMembers): TeamMemberTreeNode => {
+        const children = childrenMap.get(member.id) || []
+        return {
+          id: member.id,
+          name: member.name,
+          userId: member.userId,
+          teamId: member.teamId,
+          commissionRate: Number(member.commissionRate),
+          totalRevenue: Number(member.totalRevenue),
+          todayRevenue: Number(member.todayRevenue),
+          promoCode: member.promoCode,
+          createdAt: member.createdAt,
+          updatedAt: member.updatedAt,
+          type: 'teamMember',
+          children: children.map(child => buildNode(child))
+        }
+      }
+
+      // 找到所有根节点(parentId 为 null 的成员)
+      const rootMembers = childrenMap.get(null) || []
+      return rootMembers.map(member => buildNode(member))
+    }
+
+    // 如果有团队信息,构建包含团队节点的树
+    // 构建成员映射(id -> TeamMembers)
+    const memberMap = new Map<number, TeamMembers>()
+    members.forEach(member => {
+      memberMap.set(member.id, member)
+    })
+
+    // 构建父子关系映射(parentId -> TeamMembers[])
+    const childrenMap = new Map<number | null, TeamMembers[]>()
+    members.forEach(member => {
+      const parentId = member.parentId
+      if (!childrenMap.has(parentId)) {
+        childrenMap.set(parentId, [])
+      }
+      childrenMap.get(parentId)!.push(member)
+    })
+
+    // 递归构建团队成员节点
+    const buildMemberNode = (member: TeamMembers): TeamMemberTreeNode => {
+      const children = childrenMap.get(member.id) || []
+      return {
+        id: member.id,
+        name: member.name,
+        userId: member.userId,
+        teamId: member.teamId,
+        commissionRate: Number(member.commissionRate),
+        totalRevenue: Number(member.totalRevenue),
+        todayRevenue: Number(member.todayRevenue),
+        promoCode: member.promoCode,
+        createdAt: member.createdAt,
+        updatedAt: member.updatedAt,
+        type: 'teamMember',
+        children: children.map(child => buildMemberNode(child))
+      }
+    }
+
+    // 找到所有根节点(parentId 为 null 的成员,这些是团队的直接下级)
+    const rootMembers = childrenMap.get(null) || []
+
+    // 构建根节点(团队)
+    const rootNode: TeamMemberTreeNode = {
+      id: 0,
+      name: team.name,
+      userId: team.userId,
+      teamId: team.id,
+      commissionRate: Number(team.commissionRate),
+      totalRevenue: Number(team.totalRevenue),
+      todayRevenue: Number(team.todayRevenue),
+      promoCode: null,
+      createdAt: team.createdAt,
+      updatedAt: team.updatedAt,
+      type: 'team',
+      children: rootMembers.map(member => buildMemberNode(member))
+    }
+
+    return [rootNode]
+  }
+
+  /**
+   * 递归获取下级节点
+   * 优化:批量查询所有下级用户和团队,减少数据库查询次数
+   */
+  private async getChildrenRecursive(
+    parentUserId: number,
+    memberMap: Map<number, TeamMembers>,
+    allUsersMap?: Map<number, User[]>,
+    allTeamsMap?: Map<number, Team>
+  ): Promise<TeamMemberTreeNode[]> {
+    const children: TeamMemberTreeNode[] = []
+
+    // 如果提供了预加载的数据,使用预加载的数据
+    let childUsers: User[] = []
+    if (allUsersMap) {
+      childUsers = allUsersMap.get(parentUserId) || []
+    } else {
+      // 否则查询数据库
+      childUsers = await this.userRepository.find({
+        where: { parentId: parentUserId }
+      })
+    }
+
+    for (const childUser of childUsers) {
+      // 检查是否是团队成员
+      const teamMember = memberMap.get(childUser.id)
+      
+      if (teamMember) {
+        // 是团队成员
+        children.push({
+          id: teamMember.id,
+          name: teamMember.name,
+          userId: teamMember.userId,
+          teamId: teamMember.teamId,
+          commissionRate: Number(teamMember.commissionRate),
+          totalRevenue: Number(teamMember.totalRevenue),
+          todayRevenue: Number(teamMember.todayRevenue),
+          promoCode: teamMember.promoCode,
+          createdAt: teamMember.createdAt,
+          updatedAt: teamMember.updatedAt,
+          type: 'teamMember',
+          children: await this.getChildrenRecursive(childUser.id, memberMap, allUsersMap, allTeamsMap)
+        })
+      } else {
+        // 检查是否是团队
+        let childTeam: Team | null = null
+        if (allTeamsMap) {
+          childTeam = allTeamsMap.get(childUser.id) || null
+        } else {
+          childTeam = await this.teamRepository.findOne({ where: { userId: childUser.id } })
+        }
+        
+        if (childTeam) {
+          // 是团队
+          children.push({
+            id: 0,
+            name: childTeam.name,
+            userId: childUser.id,
+            teamId: childTeam.id,
+            commissionRate: Number(childTeam.commissionRate),
+            totalRevenue: Number(childTeam.totalRevenue),
+            todayRevenue: Number(childTeam.todayRevenue),
+            promoCode: null,
+            createdAt: childTeam.createdAt,
+            updatedAt: childTeam.updatedAt,
+            type: 'team',
+            children: await this.getChildrenRecursive(childUser.id, memberMap, allUsersMap, allTeamsMap)
+          })
+        } else {
+          // 其他类型的用户(可能是推广员或其他)
+          children.push({
+            id: 0,
+            name: childUser.name,
+            userId: childUser.id,
+            teamId: 0,
+            commissionRate: 0,
+            totalRevenue: 0,
+            todayRevenue: 0,
+            promoCode: null,
+            createdAt: childUser.createdAt,
+            updatedAt: childUser.updatedAt,
+            type: 'other',
+            children: await this.getChildrenRecursive(childUser.id, memberMap, allUsersMap, allTeamsMap)
+          })
+        }
+      }
+    }
+
+    return children
+  }
+
 
   async update(data: UpdateTeamMembersBody): Promise<TeamMembers> {
-    const { id, ...updateData } = data
+    const { id, memberId, ...updateData } = data
+
+    // 获取当前成员信息
+    const currentMember = await this.findById(id)
+
+    // 构建更新数据对象
+    const finalUpdateData: any = { ...updateData }
+
+    // 确定最终的父级ID(如果修改了 memberId,使用新的;否则使用当前的)
+    let finalParentId: number | null = currentMember.parentId
+    let parentTeamMember: TeamMembers | null = null
+
+    // 处理 memberId(更新 parentId)
+    if (memberId !== undefined) {
+      if (memberId === null) {
+        // 设置为 null,表示成为团队的直接下级
+        finalUpdateData.parentId = null
+        finalParentId = null
+      } else {
+        // 验证父级团队成员是否存在
+        parentTeamMember = await this.teamMembersRepository.findOne({ where: { id: memberId } })
+        if (!parentTeamMember) {
+          throw new Error('父级团队成员不存在')
+        }
+
+        // 验证不能设置为自己
+        if (memberId === id) {
+          throw new Error('不能将自己设置为父级')
+        }
+
+        // 验证不能设置为自己的子节点(避免循环引用)
+        const isDescendant = await this.isDescendant(id, memberId)
+        if (isDescendant) {
+          throw new Error('不能将自己的子节点设置为父级')
+        }
 
-    // 验证分成比例
+        // 验证父级团队成员必须属于同一个团队
+        if (parentTeamMember.teamId !== currentMember.teamId) {
+          throw new Error('父级团队成员必须属于同一个团队')
+        }
+
+        finalUpdateData.parentId = memberId
+        finalParentId = memberId
+      }
+    } else {
+      // 如果没有修改 parentId,使用当前的父级ID来验证分润比例
+      if (currentMember.parentId !== null) {
+        parentTeamMember = await this.teamMembersRepository.findOne({ where: { id: currentMember.parentId } })
+      }
+    }
+
+    // 验证分成比例(如果有父级,需要验证不能大于父级比例)
     if (updateData.commissionRate !== undefined && updateData.commissionRate > 0) {
-      // 获取当前成员信息以获取teamId
-      const currentMember = await this.findById(id)
-      await this.validateCommissionRate(currentMember.teamId, updateData.commissionRate, id)
+      await this.validateCommissionRate(
+        currentMember.teamId, 
+        updateData.commissionRate, 
+        id,
+        finalParentId
+      )
     }
 
-    await this.teamMembersRepository.update(id, updateData)
+    await this.teamMembersRepository.update(id, finalUpdateData)
     return this.findById(id)
   }
 
+  /**
+   * 检查 targetId 是否是 ancestorId 的后代节点
+   * 用于防止循环引用
+   */
+  private async isDescendant(ancestorId: number, targetId: number): Promise<boolean> {
+    // 获取目标节点的所有祖先节点
+    let currentId: number | null = targetId
+    const visited = new Set<number>()
+
+    while (currentId !== null) {
+      if (visited.has(currentId)) {
+        // 检测到循环,返回 false(这种情况不应该发生,但作为安全措施)
+        break
+      }
+      visited.add(currentId)
+
+      if (currentId === ancestorId) {
+        return true
+      }
+
+      const member = await this.teamMembersRepository.findOne({
+        where: { id: currentId },
+        select: ['parentId']
+      })
+
+      if (!member || member.parentId === null) {
+        break
+      }
+
+      currentId = member.parentId
+    }
+
+    return false
+  }
+
   async delete(id: number): Promise<void> {
     await this.teamMembersRepository.delete(id)
   }
@@ -532,7 +909,7 @@ export class TeamMembersService {
   }
 
   /**
-   * 获取团队统计数据(队长视角)
+   * 获取团队统计数据(队长视角)- 返回树状结构
    */
   async getTeamLeaderStats(query: TeamLeaderStatsQuery, teamLeaderUserId: number): Promise<TeamLeaderStatsResponse> {
     const { teamId } = query
@@ -554,83 +931,146 @@ export class TeamMembersService {
     const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate())
     const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59)
 
-    // 一次性查询团队成员的全部收入记录
+    // 获取团队成员的用户ID列表
     const memberUserIds = members.map(m => m.userId)
-    const allMemberIncomeRecords = await this.incomeRecordsRepository
-      .createQueryBuilder('record')
-      .where('record.personalAgentId IN (:...memberUserIds)', { memberUserIds })
-      .andWhere('record.delFlag = :delFlag', { delFlag: false })
-      .andWhere('record.status = :status', { status: true })
-      .getMany()
 
-    // 按 personalAgentId 分组
-    const recordsByAgent = new Map<number, any[]>()
-    for (const rec of allMemberIncomeRecords) {
-      const key = Number(rec.personalAgentId)
-      if (!recordsByAgent.has(key)) recordsByAgent.set(key, [])
-      recordsByAgent.get(key)!.push(rec)
-    }
+    // 检查索引表中是否有数据(用于调试)
+    const indexTableCount = await this.indexRepository.count()
+    this.app.log.info({
+      teamId,
+      teamLeaderUserId,
+      memberCount: members.length,
+      memberUserIds,
+      indexTableCount
+    }, 'getTeamLeaderStats: 开始统计,检查索引表数据')
+
+    // 构建成员统计映射
+    const memberStatsMap = new Map<number, {
+      totalRevenue: number
+      todayRevenue: number
+      totalSales: number
+      todaySales: number
+      teamLeaderTotalIncome: number
+      teamLeaderTodayIncome: number
+    }>()
+
+    // 为每个成员计算统计数据(从索引表查询,如果索引表没有数据则回退到 income_records)
+    for (const member of members) {
+      // 1. 先尝试从索引表查询该成员的所有分润记录
+      // 注意:不区分 level(1、2、3等),团队成员的所有分润都要统计
+      let memberIndexRecords = await this.indexRepository
+        .createQueryBuilder('index')
+        .where('index.agentId = :agentId', { agentId: member.userId })
+        .andWhere('index.status = :status', { status: true })
+        .getMany()
+      
+      // 如果查询不到数据,可能是因为索引表中的 agentId 对应的是团队用户ID
+      // 尝试通过 income_records 表的 personalAgentId 查询
+      if (memberIndexRecords.length === 0) {
+        // 从 income_records 表查询该成员的记录(通过 personalAgentId)
+        const incomeRecords = await this.incomeRecordsRepository
+          .createQueryBuilder('record')
+          .where('record.personalAgentId = :personalAgentId', { personalAgentId: member.userId })
+          .andWhere('record.delFlag = :delFlag', { delFlag: false })
+          .andWhere('record.status = :status', { status: true })
+          .andWhere('record.personalIncomeAmount > 0')
+          .getMany()
+
+        // 将 income_records 转换为类似索引表的结构
+        memberIndexRecords = incomeRecords.map(record => ({
+          id: record.id,
+          agentId: member.userId,
+          commissionAmount: Number(record.personalIncomeAmount || 0),
+          orderNo: record.orderNo,
+          orderPrice: Number(record.orderPrice || 0),
+          userId: record.userId,
+          createdAt: record.createdAt,
+          status: record.status
+        } as any))
+      }
 
-    // 为每个成员计算统计数据(内存聚合,无额外查询)
-    const membersStats = await Promise.all(
-      members.map(async (member) => {
-        const memberRecords = recordsByAgent.get(member.userId) || []
+      // 2. 计算总收入(该成员获得的所有分润)
+      const totalRevenue = memberIndexRecords.reduce((sum, r) => sum + Number(r.commissionAmount || 0), 0)
+
+      // 3. 计算今日收入
+      const todayRevenue = memberIndexRecords
+        .filter(r => {
+          const d = new Date(r.createdAt)
+          return d >= todayStart && d <= todayEnd
+        })
+        .reduce((sum, r) => sum + Number(r.commissionAmount || 0), 0)
+
+      // 4. 计算总销售额(按订单号去重)
+      const uniqueOrders = new Set<string>()
+      const totalSales = memberIndexRecords.reduce((sum, r) => {
+        if (!uniqueOrders.has(r.orderNo)) {
+          uniqueOrders.add(r.orderNo)
+          return sum + Number(r.orderPrice || 0)
+        }
+        return sum
+      }, 0)
 
-        const totalRevenue = memberRecords
-          .filter(r => Number(r.personalIncomeAmount || 0) > 0)
-          .reduce((sum, r) => sum + Number(r.personalIncomeAmount || 0), 0)
+      // 5. 计算今日销售额(按订单号去重)
+      const todayUniqueOrders = new Set<string>()
+      const todaySales = memberIndexRecords
+        .filter(r => {
+          const d = new Date(r.createdAt)
+          return d >= todayStart && d <= todayEnd
+        })
+        .reduce((sum, r) => {
+          if (!todayUniqueOrders.has(r.orderNo)) {
+            todayUniqueOrders.add(r.orderNo)
+            return sum + Number(r.orderPrice || 0)
+          }
+          return sum
+        }, 0)
 
-        const todayRevenue = memberRecords
-          .filter(r => Number(r.personalIncomeAmount || 0) > 0)
-          .filter(r => {
-            const d = new Date(r.createdAt)
-            return d >= todayStart && d <= todayEnd
-          })
-          .reduce((sum, r) => sum + Number(r.personalIncomeAmount || 0), 0)
+      // 6. 查询该成员的下级用户列表(通过 income_records 表的 personalAgentId)
+      const memberOrderUserIds = await this.incomeRecordsRepository
+        .createQueryBuilder('record')
+        .select('DISTINCT record.userId', 'userId')
+        .where('record.personalAgentId = :personalAgentId', { personalAgentId: member.userId })
+        .andWhere('record.delFlag = :delFlag', { delFlag: false })
+        .andWhere('record.status = :status', { status: true })
+        .getRawMany()
+
+      const orderUserIds = memberOrderUserIds.map(item => item.userId).filter(id => id !== null && id !== undefined)
+
+      // 7. 计算队长从该成员获得的实际收入(队长在这些订单中获得的分润)
+      let teamLeaderTotalIncome = 0
+      let teamLeaderTodayIncome = 0
+
+      if (orderUserIds.length > 0) {
+        // 查询队长在这些订单中获得的分润
+        const leaderIndexRecords = await this.indexRepository
+          .createQueryBuilder('index')
+          .where('index.agentId = :agentId', { agentId: teamLeaderUserId })
+          .andWhere('index.userId IN (:...userIds)', { userIds: orderUserIds })
+          .andWhere('index.status = :status', { status: true })
+          .getMany()
 
-        const totalSales = memberRecords.reduce((sum, r) => sum + Number(r.orderPrice || 0), 0)
+        teamLeaderTotalIncome = leaderIndexRecords.reduce((sum, r) => sum + Number(r.commissionAmount || 0), 0)
 
-        const todaySales = memberRecords
+        teamLeaderTodayIncome = leaderIndexRecords
           .filter(r => {
             const d = new Date(r.createdAt)
             return d >= todayStart && d <= todayEnd
           })
-          .reduce((sum, r) => sum + Number(r.orderPrice || 0), 0)
-
-        // 计算队长从该成员获得的实际收入(该成员订单中队长分得的部分)
-        const teamLeaderTotalIncome = memberRecords.reduce((sum, record) => {
-          const leaderPart = Number(record.incomeAmount || 0)
-          return sum + leaderPart
-        }, 0)
-
-        const teamLeaderTodayIncome = memberRecords
-          .filter(record => {
-            const recordDate = new Date(record.createdAt)
-            return recordDate >= todayStart && recordDate <= todayEnd
-          })
-          .reduce((sum, record) => {
-            const leaderPart = Number(record.incomeAmount || 0)
-            return sum + leaderPart
-          }, 0)
-
-        const personalCommissionRate = Number(member.commissionRate || 0)
-        const teamCommissionRate = Number(team.commissionRate || 0)
-        const memberActualRate = teamCommissionRate - personalCommissionRate
+          .reduce((sum, r) => sum + Number(r.commissionAmount || 0), 0)
+      }
 
-        return {
-          memberId: member.id,
-          memberName: member.name,
-          personalCommissionRate: Number(personalCommissionRate.toFixed(2)),
-          actualRate: Number(memberActualRate.toFixed(2)),
-          totalRevenue: Number(totalRevenue.toFixed(5)),
-          todayRevenue: Number(todayRevenue.toFixed(5)),
-          totalSales: Number(totalSales.toFixed(5)),
-          todaySales: Number(todaySales.toFixed(5)),
-          teamLeaderTotalIncome: Number(teamLeaderTotalIncome.toFixed(5)),
-          teamLeaderTodayIncome: Number(teamLeaderTodayIncome.toFixed(5))
-        }
+      memberStatsMap.set(member.id, {
+        totalRevenue: Number(totalRevenue.toFixed(5)),
+        todayRevenue: Number(todayRevenue.toFixed(5)),
+        totalSales: Number(totalSales.toFixed(5)),
+        todaySales: Number(todaySales.toFixed(5)),
+        teamLeaderTotalIncome: Number(teamLeaderTotalIncome.toFixed(5)),
+        teamLeaderTodayIncome: Number(teamLeaderTodayIncome.toFixed(5))
       })
-    )
+    }
+
+    // 构建树状结构
+    const membersStats = await this.buildTeamMemberStatsTree(members, team, memberStatsMap)
 
     return {
       teamId: team.id,
@@ -640,6 +1080,94 @@ export class TeamMembersService {
     }
   }
 
+  /**
+   * 构建团队成员统计树状结构
+   */
+  private async buildTeamMemberStatsTree(
+    members: TeamMembers[],
+    team: Team,
+    memberStatsMap: Map<number, {
+      totalRevenue: number
+      todayRevenue: number
+      totalSales: number
+      todaySales: number
+      teamLeaderTotalIncome: number
+      teamLeaderTodayIncome: number
+    }>
+  ): Promise<TeamMemberStatsTreeNode[]> {
+    // 构建成员映射(id -> TeamMembers)
+    const memberMap = new Map<number, TeamMembers>()
+    members.forEach(member => {
+      memberMap.set(member.id, member)
+    })
+
+    // 构建父子关系映射(parentId -> TeamMembers[])
+    const childrenMap = new Map<number | null, TeamMembers[]>()
+    members.forEach(member => {
+      const parentId = member.parentId
+      if (!childrenMap.has(parentId)) {
+        childrenMap.set(parentId, [])
+      }
+      childrenMap.get(parentId)!.push(member)
+    })
+
+    // 递归构建团队成员统计节点
+    const buildMemberNode = (member: TeamMembers): TeamMemberStatsTreeNode => {
+      const children = childrenMap.get(member.id) || []
+      const stats = memberStatsMap.get(member.id) || {
+        totalRevenue: 0,
+        todayRevenue: 0,
+        totalSales: 0,
+        todaySales: 0,
+        teamLeaderTotalIncome: 0,
+        teamLeaderTodayIncome: 0
+      }
+
+      return {
+        id: member.id,
+        name: member.name,
+        userId: member.userId,
+        teamId: member.teamId,
+        commissionRate: Number(member.commissionRate),
+        parentId: member.parentId,
+        totalRevenue: stats.totalRevenue,
+        todayRevenue: stats.todayRevenue,
+        totalSales: stats.totalSales,
+        todaySales: stats.todaySales,
+        todayDAU: 0,
+        todayNewUsers: 0,
+        teamLeaderTotalIncome: stats.teamLeaderTotalIncome,
+        teamLeaderTodayIncome: stats.teamLeaderTodayIncome,
+        type: 'teamMember',
+        children: children.map(child => buildMemberNode(child))
+      }
+    }
+
+    // 找到所有根节点(parentId 为 null 的成员,这些是团队的直接下级)
+    const rootMembers = childrenMap.get(null) || []
+
+    // 构建根节点(团队)
+    const rootNode: TeamMemberStatsTreeNode = {
+      id: 0,
+      name: team.name,
+      userId: team.userId,
+      teamId: team.id,
+      commissionRate: Number(team.commissionRate),
+      parentId: null,
+      totalRevenue: 0,
+      todayRevenue: 0,
+      totalSales: 0,
+      todaySales: 0,
+      todayDAU: 0,
+      todayNewUsers: 0,
+      type: 'team',
+      children: rootMembers.map(member => buildMemberNode(member))
+    }
+
+    return [rootNode]
+  }
+
+
   /**
    * 生成团队成员的推广链接
    * @param teamMemberId 团队成员ID
@@ -689,7 +1217,7 @@ export class TeamMembersService {
       })
     } catch (error) {
       // 如果创建或更新记录失败,记录日志但不影响返回链接
-      this.app.log.warn('创建或更新推广链接记录失败:', error)
+      this.app.log.warn({ err: error }, '创建或更新推广链接记录失败')
     }
     
     return promotionLink
@@ -888,29 +1416,11 @@ export class TeamMembersService {
           .getCount()
       : 0
 
-    // 统计总收入(只统计personalIncomeAmount)
-    const totalIncomeRecords = await this.incomeRecordsRepository
-      .createQueryBuilder('record')
-      .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-      .where('record.personalAgentId = :personalAgentId', { personalAgentId: userId })
-      .andWhere('record.delFlag = :delFlag', { delFlag: false })
-      .andWhere('record.status = :status', { status: true })
-      .getRawOne()
-
-    const totalIncome = totalIncomeRecords?.totalIncome ? parseFloat(totalIncomeRecords.totalIncome) : 0
-
-    // 统计今日收入(只统计personalIncomeAmount)
-    const todayIncomeRecords = await this.incomeRecordsRepository
-      .createQueryBuilder('record')
-      .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-      .where('record.personalAgentId = :personalAgentId', { personalAgentId: userId })
-      .andWhere('record.createdAt >= :startDate', { startDate: today })
-      .andWhere('record.createdAt < :endDate', { endDate: tomorrow })
-      .andWhere('record.delFlag = :delFlag', { delFlag: false })
-      .andWhere('record.status = :status', { status: true })
-      .getRawOne()
+    // 统计总收入(使用索引表,统计该代理的所有分润)
+    const totalIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(userId)
 
-    const todayIncome = todayIncomeRecords?.totalIncome ? parseFloat(todayIncomeRecords.totalIncome) : 0
+    // 统计今日收入(使用索引表,统计该代理的所有分润)
+    const todayIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(userId, today, tomorrow)
 
     // 统计历史总销售额
     const totalSalesRecords = await this.incomeRecordsRepository
@@ -996,29 +1506,11 @@ export class TeamMembersService {
           .getCount()
       : 0
 
-    // 统计今日收入(只统计personalIncomeAmount)
-    const todayIncomeRecords = await this.incomeRecordsRepository
-      .createQueryBuilder('record')
-      .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-      .where('record.personalAgentId = :personalAgentId', { personalAgentId: userId })
-      .andWhere('record.createdAt >= :startDate', { startDate: today })
-      .andWhere('record.createdAt < :endDate', { endDate: tomorrow })
-      .andWhere('record.delFlag = :delFlag', { delFlag: false })
-      .andWhere('record.status = :status', { status: true })
-      .getRawOne()
-
-    const todayIncome = todayIncomeRecords?.totalIncome ? parseFloat(todayIncomeRecords.totalIncome) : 0
-
-    // 统计历史总收入(只统计personalIncomeAmount)
-    const totalIncomeRecords = await this.incomeRecordsRepository
-      .createQueryBuilder('record')
-      .select('SUM(CAST(record.personalIncomeAmount AS DECIMAL(10,5)))', 'totalIncome')
-      .where('record.personalAgentId = :personalAgentId', { personalAgentId: userId })
-      .andWhere('record.delFlag = :delFlag', { delFlag: false })
-      .andWhere('record.status = :status', { status: true })
-      .getRawOne()
+    // 统计今日收入(使用索引表,统计该代理的所有分润)
+    const todayIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(userId, today, tomorrow)
 
-    const totalIncome = totalIncomeRecords?.totalIncome ? parseFloat(totalIncomeRecords.totalIncome) : 0
+    // 统计历史总收入(使用索引表,统计该代理的所有分润)
+    const totalIncome = await this.multiLevelCommissionService.getAgentTotalCommissionByDateRange(userId)
 
     // 统计历史总销售额
     const totalSalesRecords = await this.incomeRecordsRepository

+ 33 - 28
src/services/team.service.ts

@@ -11,6 +11,7 @@ import { CreateTeamBody, UpdateTeamBody, ListTeamQuery } from '../dto/team.dto'
 import { UserService } from './user.service'
 import { SysConfigService } from './sys-config.service'
 import { UserRole } from '../entities/user.entity'
+import { MultiLevelCommissionService } from './multi-level-commission.service'
 import * as randomstring from 'randomstring'
 
 export class TeamService {
@@ -22,6 +23,7 @@ export class TeamService {
   private teamDomainRepository: Repository<TeamDomain>
   private userService: UserService
   private sysConfigService: SysConfigService
+  private multiLevelCommissionService: MultiLevelCommissionService
 
   constructor(app: FastifyInstance) {
     this.teamRepository = app.dataSource.getRepository(Team)
@@ -32,6 +34,7 @@ export class TeamService {
     this.teamDomainRepository = app.dataSource.getRepository(TeamDomain)
     this.userService = new UserService(app)
     this.sysConfigService = new SysConfigService(app)
+    this.multiLevelCommissionService = new MultiLevelCommissionService(app)
   }
 
   async create(data: CreateTeamBody, creatorId: number): Promise<Team> {
@@ -169,16 +172,15 @@ export class TeamService {
     
     // 获取所有团队的 ID 列表,用于查询会员数据
     const teamIds = teams.map(team => team.id)
+    // 只有在管理员查看时才添加默认的 teamId 0(默认渠道)
+    const allTeamIds = userId ? teamIds : [...teamIds, 0]
 
-    // 查询所有团队的总收入统计(包括团队分成和个人分成)
-    const totalRevenueStats =
+    // 查询所有团队的总收入统计(使用 incomeAmount,统计团队分润
+    const totalRevenueStatsRaw =
       allUserIds.length > 0
         ? await this.incomeRecordsRepository
             .createQueryBuilder('record')
-            .select([
-              'record.agentId as userId', 
-              'SUM(record.incomeAmount + record.personalIncomeAmount) as totalRevenue'
-            ])
+            .select(['record.agentId as userId', 'SUM(record.incomeAmount) as totalRevenue'])
             .where('record.delFlag = :delFlag', { delFlag: false })
             .andWhere('record.status = :status', { status: true })
             .andWhere('record.agentId IN (:...allUserIds)', { allUserIds })
@@ -186,15 +188,17 @@ export class TeamService {
             .getRawMany()
         : []
 
-    // 查询所有团队的今日收入统计(包括团队分成和个人分成)
-    const todayRevenueStats =
+    const totalRevenueStats = new Map<number, number>()
+    totalRevenueStatsRaw.forEach((stat: any) => {
+      totalRevenueStats.set(stat.userId, Number(stat.totalRevenue) || 0)
+    })
+
+    // 查询所有团队的今日收入统计(使用 incomeAmount,统计团队分润)
+    const todayRevenueStatsRaw =
       allUserIds.length > 0
         ? await this.incomeRecordsRepository
             .createQueryBuilder('record')
-            .select([
-              'record.agentId as userId', 
-              'SUM(record.incomeAmount + record.personalIncomeAmount) as todayRevenue'
-            ])
+            .select(['record.agentId as userId', 'SUM(record.incomeAmount) as todayRevenue'])
             .where('record.delFlag = :delFlag', { delFlag: false })
             .andWhere('record.status = :status', { status: true })
             .andWhere('record.createdAt >= :today', { today })
@@ -204,6 +208,11 @@ export class TeamService {
             .getRawMany()
         : []
 
+    const todayRevenueStats = new Map<number, number>()
+    todayRevenueStatsRaw.forEach((stat: any) => {
+      todayRevenueStats.set(stat.userId, Number(stat.todayRevenue) || 0)
+    })
+
     // 查询所有团队的总售卖金额统计(通过 userId 关联,包括默认的 agentId 0)
     const totalSalesStats =
       allUserIds.length > 0
@@ -233,56 +242,52 @@ export class TeamService {
         : []
 
     // 查询今日日活统计(基于会员的lastLoginAt字段)
-    const todayDAUStats = teamIds.length > 0
+    // 需要包含默认渠道(teamId = 0)的数据,如果是管理员查看
+    const todayDAUStats = allTeamIds.length > 0
       ? await this.memberRepository
           .createQueryBuilder('member')
           .select(['member.teamId as teamId', 'COUNT(DISTINCT member.userId) as dau'])
           .where('member.lastLoginAt >= :today', { today })
           .andWhere('member.lastLoginAt < :tomorrow', { tomorrow })
-          .andWhere('member.teamId IN (:...teamIds)', { teamIds })
+          .andWhere('member.teamId IN (:...allTeamIds)', { allTeamIds })
           .groupBy('member.teamId')
           .getRawMany()
       : []
 
     // 查询今日新增用户统计(基于会员的createdAt字段)
-    const todayNewUsersStats = teamIds.length > 0
+    // 需要包含默认渠道(teamId = 0)的数据,如果是管理员查看
+    const todayNewUsersStats = allTeamIds.length > 0
       ? await this.memberRepository
           .createQueryBuilder('member')
           .select(['member.teamId as teamId', 'COUNT(member.id) as newUsers'])
           .where('member.createdAt >= :today', { today })
           .andWhere('member.createdAt < :tomorrow', { tomorrow })
-          .andWhere('member.teamId IN (:...teamIds)', { teamIds })
+          .andWhere('member.teamId IN (:...allTeamIds)', { allTeamIds })
           .groupBy('member.teamId')
           .getRawMany()
       : []
 
     // 查询总用户数统计(基于会员的teamId)
-    const totalUsersStats = teamIds.length > 0
+    // 需要包含默认渠道(teamId = 0)的数据,如果是管理员查看
+    const totalUsersStats = allTeamIds.length > 0
       ? await this.memberRepository
           .createQueryBuilder('member')
           .select(['member.teamId as teamId', 'COUNT(member.id) as totalUsers'])
-          .where('member.teamId IN (:...teamIds)', { teamIds })
+          .where('member.teamId IN (:...allTeamIds)', { allTeamIds })
           .groupBy('member.teamId')
           .getRawMany()
       : []
 
     // 构建统计数据映射(使用 userId 作为键)
-    const totalRevenueMap = new Map<number, number>()
-    const todayRevenueMap = new Map<number, number>()
+    // totalRevenueStats 和 todayRevenueStats 已经是 Map 类型
+    const totalRevenueMap = totalRevenueStats
+    const todayRevenueMap = todayRevenueStats
     const totalSalesMap = new Map<number, number>()
     const todaySalesMap = new Map<number, number>()
     const todayDAUMap = new Map<number, number>()
     const todayNewUsersMap = new Map<number, number>()
     const totalUsersMap = new Map<number, number>()
 
-    totalRevenueStats.forEach(stat => {
-      totalRevenueMap.set(stat.userId, Number(stat.totalRevenue) || 0)
-    })
-
-    todayRevenueStats.forEach(stat => {
-      todayRevenueMap.set(stat.userId, Number(stat.todayRevenue) || 0)
-    })
-
     totalSalesStats.forEach(stat => {
       totalSalesMap.set(stat.userId, Number(stat.totalSales) || 0)
     })

+ 170 - 25
src/services/user.service.ts

@@ -116,45 +116,190 @@ export class UserService {
     page: number = 0,
     size: number = 20
   ): Promise<PaginationResponse<Partial<User>>> {
-    const result: Partial<User>[] = []
+    const pageNum = Number(page) || 0
+    const sizeNum = Number(size) || 20
+    const offset = pageNum * sizeNum
 
-    const currentUser = await this.userRepository.findOne({
-      select: ['id', 'name', 'role', 'parentId', 'createdAt', 'updatedAt'],
-      where: { id: parentId }
-    })
+    const queryRunner = this.userRepository.manager.connection.createQueryRunner()
+    
+    try {
+      // 获取表名(使用反引号包裹,因为 user 可能是 MySQL 保留字)
+      const tableName = this.userRepository.metadata.tableName
+      const escapedTableName = `\`${tableName}\``
+
+      // 检测MySQL版本,MySQL 8.0+支持递归CTE
+      const versionResult = await queryRunner.query('SELECT VERSION() as version')
+      const version = versionResult[0]?.version || ''
+      const majorVersion = parseInt(version.split('.')[0]) || 0
+      const minorVersion = parseInt(version.split('.')[1]) || 0
+      const supportsRecursiveCTE = majorVersion > 8 || (majorVersion === 8 && minorVersion >= 0)
 
-    if (currentUser) {
-      result.push(currentUser)
+      if (supportsRecursiveCTE) {
+        // MySQL 8.0+ 使用递归CTE
+        return await this.findAllChildUsersWithRecursiveCTE(queryRunner, escapedTableName, parentId, pageNum, sizeNum, offset)
+      } else {
+        // MySQL 5.7及以下使用递归查询替代方案
+        return await this.findAllChildUsersWithIteration(queryRunner, escapedTableName, parentId, pageNum, sizeNum, offset)
+      }
+    } catch (error) {
+      // 如果递归CTE失败,回退到迭代方案
+      try {
+        const tableName = this.userRepository.metadata.tableName
+        const escapedTableName = `\`${tableName}\``
+        return await this.findAllChildUsersWithIteration(queryRunner, escapedTableName, parentId, pageNum, sizeNum, offset)
+      } catch (fallbackError) {
+        throw new Error(`查询用户列表失败: ${error instanceof Error ? error.message : String(error)}`)
+      }
+    } finally {
+      await queryRunner.release()
     }
+  }
 
-    const findChildren = async (pid: number) => {
-      const users = await this.userRepository.find({
-        select: ['id', 'name', 'role', 'parentId', 'createdAt', 'updatedAt'],
-        where: { parentId: pid }
-      })
+  private async findAllChildUsersWithRecursiveCTE(
+    queryRunner: any,
+    escapedTableName: string,
+    parentId: number,
+    pageNum: number,
+    sizeNum: number,
+    offset: number
+  ): Promise<PaginationResponse<Partial<User>>> {
+    // 先获取总数
+    const totalQuery = `
+      WITH RECURSIVE user_tree AS (
+        -- 基础查询:当前用户
+        SELECT id, name, role, parentId, createdAt, updatedAt
+        FROM ${escapedTableName}
+        WHERE id = ?
+        
+        UNION ALL
+        
+        -- 递归查询:所有子用户
+        SELECT u.id, u.name, u.role, u.parentId, u.createdAt, u.updatedAt
+        FROM ${escapedTableName} u
+        INNER JOIN user_tree ut ON u.parentId = ut.id
+      )
+      SELECT COUNT(*) as total FROM user_tree
+    `
+    
+    const totalResult = await queryRunner.query(totalQuery, [parentId])
+    const total = parseInt(totalResult[0]?.total || '0', 10)
 
-      for (const user of users) {
-        result.push(user)
-        await findChildren(user.id)
+    // 获取分页数据
+    const dataQuery = `
+      WITH RECURSIVE user_tree AS (
+        -- 基础查询:当前用户
+        SELECT id, name, role, parentId, createdAt, updatedAt
+        FROM ${escapedTableName}
+        WHERE id = ?
+        
+        UNION ALL
+        
+        -- 递归查询:所有子用户
+        SELECT u.id, u.name, u.role, u.parentId, u.createdAt, u.updatedAt
+        FROM ${escapedTableName} u
+        INNER JOIN user_tree ut ON u.parentId = ut.id
+      )
+      SELECT id, name, role, parentId, createdAt, updatedAt
+      FROM user_tree
+      ORDER BY id ASC
+      LIMIT ? OFFSET ?
+    `
+    
+    const users = await queryRunner.query(dataQuery, [parentId, sizeNum, offset])
+
+    return {
+      content: users.map((user: any) => ({
+        id: user.id,
+        name: user.name,
+        role: user.role,
+        parentId: user.parentId,
+        createdAt: user.createdAt,
+        updatedAt: user.updatedAt
+      })),
+      metadata: {
+        total,
+        page: pageNum,
+        size: sizeNum
       }
     }
+  }
 
-    await findChildren(parentId)
+  private async findAllChildUsersWithIteration(
+    queryRunner: any,
+    escapedTableName: string,
+    parentId: number,
+    pageNum: number,
+    sizeNum: number,
+    offset: number
+  ): Promise<PaginationResponse<Partial<User>>> {
+    // 使用迭代方式收集所有子用户ID(兼容MySQL 5.7)
+    const allUserIds: number[] = [parentId] // 先添加父用户本身
+    const processedIds = new Set<number>([parentId])
+    let currentLevelIds = [parentId]
+
+    while (currentLevelIds.length > 0) {
+      const placeholders = currentLevelIds.map(() => '?').join(',')
+      const query = `
+        SELECT id, name, role, parentId, createdAt, updatedAt
+        FROM ${escapedTableName}
+        WHERE parentId IN (${placeholders})
+      `
+      
+      const children = await queryRunner.query(query, currentLevelIds)
+      
+      // 准备下一层级
+      const nextLevelIds: number[] = []
+      for (const child of children) {
+        if (!processedIds.has(child.id)) {
+          allUserIds.push(child.id)
+          processedIds.add(child.id)
+          nextLevelIds.push(child.id)
+        }
+      }
+      
+      currentLevelIds = nextLevelIds
+    }
 
-    result.sort((a, b) => (a.id || 0) - (b.id || 0))
+    const total = allUserIds.length
 
-    const total = result.length
-    const paginatedUsers = result.slice(
-      (Number(page) || 0) * (Number(size) || 20),
-      ((Number(page) || 0) + 1) * (Number(size) || 20)
-    )
+    // 分页处理
+    const paginatedIds = allUserIds.slice(offset, offset + sizeNum)
+    
+    if (paginatedIds.length === 0) {
+      return {
+        content: [],
+        metadata: {
+          total,
+          page: pageNum,
+          size: sizeNum
+        }
+      }
+    }
+
+    // 根据ID获取用户详细信息
+    const placeholders = paginatedIds.map(() => '?').join(',')
+    const dataQuery = `
+      SELECT id, name, role, parentId, createdAt, updatedAt
+      FROM ${escapedTableName}
+      WHERE id IN (${placeholders})
+      ORDER BY id ASC
+    `
+    
+    const users = await queryRunner.query(dataQuery, paginatedIds)
 
     return {
-      content: paginatedUsers,
+      content: users.map((user: any) => ({
+        id: user.id,
+        name: user.name,
+        role: user.role,
+        parentId: user.parentId,
+        createdAt: user.createdAt,
+        updatedAt: user.updatedAt
+      })),
       metadata: {
         total,
-        page: Number(page) || 0,
-        size: Number(size) || 20
+        page: pageNum,
+        size: sizeNum
       }
     }
   }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 252 - 206
yarn.lock


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott