瀏覽代碼

添加视频源

wilhelm wong 3 周之前
父節點
當前提交
24f64c7c80

+ 118 - 0
docs/video-url-helper.ts

@@ -0,0 +1,118 @@
+/**
+ * 视频播放地址处理工具函数(TypeScript版本)
+ * 用于前端处理HTTP资源在HTTPS网站中的访问问题
+ * 
+ * 使用方法:
+ * import { getVideoPlayUrl, processVideoList } from './video-url-helper'
+ * 
+ * const playUrl = getVideoPlayUrl('http://example.com/video.m3u8')
+ */
+
+export interface VideoItem {
+  id: number
+  title: string
+  m3u8: string
+  image?: string
+  [key: string]: any
+}
+
+export interface VideoUrlOptions {
+  baseUrl?: string
+  forceProxy?: boolean
+}
+
+/**
+ * 获取视频播放地址(自动处理HTTP资源)
+ * @param m3u8Url 后端返回的m3u8地址
+ * @param options 配置选项
+ * @returns 处理后的播放地址
+ */
+export function getVideoPlayUrl(
+  m3u8Url: string,
+  options: VideoUrlOptions = {}
+): string {
+  if (!m3u8Url || typeof m3u8Url !== 'string') {
+    return m3u8Url
+  }
+
+  try {
+    const url = new URL(m3u8Url)
+    const baseUrl = options.baseUrl || window.location.origin
+    
+    // 强制使用代理
+    if (options.forceProxy) {
+      const path = url.pathname + url.search + url.hash
+      return `${baseUrl}/api/proxy/m3u8${path}`
+    }
+    
+    // HTTPS直接使用
+    if (url.protocol === 'https:') {
+      return m3u8Url
+    }
+    
+    // HTTP转换为代理地址
+    if (url.protocol === 'http:') {
+      const path = url.pathname + url.search + url.hash
+      return `${baseUrl}/api/proxy/m3u8${path}`
+    }
+    
+    // 其他情况,直接返回
+    return m3u8Url
+  } catch (error) {
+    console.warn('处理视频地址失败:', error, m3u8Url)
+    return m3u8Url
+  }
+}
+
+/**
+ * 批量处理视频列表的播放地址
+ * @param videos 视频列表数组
+ * @param options 配置选项
+ * @returns 处理后的视频列表
+ */
+export function processVideoList<T extends VideoItem>(
+  videos: T[],
+  options: VideoUrlOptions = {}
+): T[] {
+  if (!Array.isArray(videos)) {
+    return videos
+  }
+  
+  return videos.map(video => {
+    if (video.m3u8) {
+      return {
+        ...video,
+        m3u8: getVideoPlayUrl(video.m3u8, options)
+      }
+    }
+    return video
+  })
+}
+
+/**
+ * 检查地址是否需要代理
+ * @param m3u8Url m3u8地址
+ * @returns 是否需要代理
+ */
+export function needsProxy(m3u8Url: string): boolean {
+  if (!m3u8Url || typeof m3u8Url !== 'string') {
+    return false
+  }
+  
+  try {
+    const url = new URL(m3u8Url)
+    return url.protocol === 'http:'
+  } catch {
+    return false
+  }
+}
+
+/**
+ * 默认导出(方便使用)
+ */
+export default {
+  getVideoPlayUrl,
+  processVideoList,
+  needsProxy
+}
+

+ 2 - 0
src/app.ts

@@ -26,6 +26,7 @@ import pageClickRecordRoutes from './routes/page-click-record.routes'
 import videoRoutes from './routes/video.routes'
 import m3u8ProxyRoutes from './routes/m3u8-proxy.routes'
 import domainManagementRoutes from './routes/domain-management.routes'
+import videoFeedbackRoutes from './routes/video-feedback.routes'
 import { authenticate } from './middlewares/auth.middleware'
 import { createRedisClient, closeRedisClient } from './config/redis'
 import { BannerStatisticsScheduler } from './scheduler/banner-statistics.scheduler'
@@ -111,6 +112,7 @@ export const createApp = async () => {
   app.register(videoRoutes, { prefix: '/api/video' })
   app.register(m3u8ProxyRoutes)
   app.register(domainManagementRoutes, { prefix: '/api/domain-management' })
+  app.register(videoFeedbackRoutes, { prefix: '/api/video-feedback' })
 
   // 添加 /account 路由重定向到用户资料
   app.get('/account', { onRequest: [authenticate] }, async (request, reply) => {

+ 139 - 8
src/controllers/finance.controller.ts

@@ -1,33 +1,164 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
 import { FinanceService } from '../services/finance.service'
 import { CreateFinanceBody, UpdateFinanceBody, ListFinanceQuery, FinanceParams } from '../dto/finance.dto'
-import { FinanceStatus } from '../entities/finance.entity'
+import { Finance, FinanceStatus } from '../entities/finance.entity'
 import { UserRole } from '../entities/user.entity'
+import { Repository } from 'typeorm'
+import { Team } from '../entities/team.entity'
+import { TeamMembers } from '../entities/team-members.entity'
+import { IncomeRecords } from '../entities/income-records.entity'
+import bcrypt from 'bcryptjs'
 
 export class FinanceController {
   private financeService: FinanceService
+  private teamRepository: Repository<Team>
+  private teamMembersRepository: Repository<TeamMembers>
+  private financeRepository: Repository<Finance>
+  private incomeRecordsRepository: Repository<IncomeRecords>
+  private app: FastifyInstance
 
   constructor(app: FastifyInstance) {
+    this.app = app
     this.financeService = new FinanceService(app)
+    this.teamRepository = app.dataSource.getRepository(Team)
+    this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
+    this.financeRepository = app.dataSource.getRepository(Finance)
+    this.incomeRecordsRepository = app.dataSource.getRepository(IncomeRecords)
   }
 
   async create(request: FastifyRequest<{ Body: CreateFinanceBody }>, reply: FastifyReply) {
     try {
-      const { teamId, userId, reminderAmount } = request.body
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      const { reminderAmount } = request.body
+
+      // 验证金额为必填且为正数
+      if (!reminderAmount || reminderAmount <= 0) {
+        return reply.code(400).send({ message: 'reminderAmount 为必填字段且必须大于 0' })
+      }
+
+      // 验证提现金额最低100元
+      const MIN_WITHDRAW_AMOUNT = 100
+      if (reminderAmount < MIN_WITHDRAW_AMOUNT) {
+        return reply.code(400).send({ 
+          message: `提现金额不能低于 ${MIN_WITHDRAW_AMOUNT} 元,当前提现金额为 ${reminderAmount.toFixed(2)}` 
+        })
+      }
+
+      let teamId: number
+      let userId: number
+
+      // 如果是团队用户,自动获取 teamId 和 userId
+      if (user.role === UserRole.TEAM) {
+        // 从 team 表中获取团队用户的 teamId
+        const team = await this.teamRepository.findOne({ where: { userId: user.id } })
+        if (!team) {
+          return reply.code(404).send({ message: '未找到该用户的团队信息' })
+        }
+        teamId = team.id
+        userId = user.id
+        request.body.teamId = teamId
+        request.body.userId = userId
+      } else {
+        // 管理员需要手动提供 teamId 和 userId
+        const { teamId: bodyTeamId, userId: bodyUserId } = request.body
+        if (!bodyTeamId || !bodyUserId) {
+          return reply.code(400).send({ message: 'teamId 和 userId 为必填字段' })
+        }
+        teamId = bodyTeamId
+        userId = bodyUserId
+      }
+
+      // 计算余额:所有收益 - 提现金额(提现中 + 已提现)
+      // 1. 查询总收益(从 incomeRecords 表统计 agentId = userId 的 incomeAmount 总和)
+      const totalRevenueResult = await this.incomeRecordsRepository
+        .createQueryBuilder('record')
+        .select('SUM(record.incomeAmount)', 'totalRevenue')
+        .where('record.agentId = :userId', { userId })
+        .andWhere('record.delFlag = :delFlag', { delFlag: false })
+        .andWhere('record.status = :status', { status: true })
+        .getRawOne()
+
+      const totalRevenue = totalRevenueResult?.totalRevenue ? Number(totalRevenueResult.totalRevenue) : 0
+
+      // 2. 查询提现金额(从 finance 表统计 status 为 PROCESSING 或 WITHDRAWN 的金额)
+      const withdrawAmountResult = await this.financeRepository
+        .createQueryBuilder('finance')
+        .select('SUM(finance.reminderAmount)', 'withdrawAmount')
+        .where('finance.teamId = :teamId', { teamId })
+        .andWhere('finance.delFlag = :delFlag', { delFlag: false })
+        .andWhere('finance.status IN (:...statuses)', { statuses: [FinanceStatus.PROCESSING, FinanceStatus.WITHDRAWN] })
+        .getRawOne()
+
+      const withdrawAmount = withdrawAmountResult?.withdrawAmount ? Number(withdrawAmountResult.withdrawAmount) : 0
+
+      // 3. 计算余额
+      const balance = Number((totalRevenue - withdrawAmount).toFixed(5))
+
+      // 4. 验证提现金额不能大于余额
+      if (reminderAmount > balance) {
+        return reply.code(400).send({ 
+          message: `提现金额不能大于余额,当前余额为 ${balance.toFixed(2)},提现金额为 ${reminderAmount.toFixed(2)}` 
+        })
+      }
 
-      // 验证必填字段
-      if (!teamId || !userId || !reminderAmount) {
-        return reply.code(400).send({ message: 'teamId、userId 和 reminderAmount 为必填字段' })
+      // 5. 验证提现密码
+      const { withdrawPassword } = request.body
+      
+      // 获取团队信息
+      const team = await this.teamRepository.findOne({ where: { id: teamId } })
+      if (!team) {
+        return reply.code(404).send({ message: '团队不存在' })
       }
 
-      // 验证金额为正数
-      if (reminderAmount <= 0) {
-        return reply.code(400).send({ message: 'reminderAmount 必须大于 0' })
+      // 判断是团队提现还是团队成员提现
+      const isTeamWithdraw = team.userId === userId
+      
+      if (isTeamWithdraw) {
+        // 团队提现:验证团队的提现密码
+        if (!team.withdrawPassword) {
+          return reply.code(400).send({ message: '请先设置团队提现密码' })
+        }
+        
+        if (!withdrawPassword) {
+          return reply.code(400).send({ message: '请提供提现密码' })
+        }
+        
+        const isPasswordValid = await bcrypt.compare(withdrawPassword, team.withdrawPassword)
+        if (!isPasswordValid) {
+          return reply.code(400).send({ message: '提现密码错误' })
+        }
+      } else {
+        // 团队成员提现:验证团队成员的提现密码
+        const teamMember = await this.teamMembersRepository.findOne({ 
+          where: { teamId, userId } 
+        })
+        
+        if (!teamMember) {
+          return reply.code(404).send({ message: '团队成员不存在' })
+        }
+        
+        if (!teamMember.withdrawPassword) {
+          return reply.code(400).send({ message: '请先设置团队成员提现密码' })
+        }
+        
+        if (!withdrawPassword) {
+          return reply.code(400).send({ message: '请提供提现密码' })
+        }
+        
+        const isPasswordValid = await bcrypt.compare(withdrawPassword, teamMember.withdrawPassword)
+        if (!isPasswordValid) {
+          return reply.code(400).send({ message: '提现密码错误' })
+        }
       }
 
       const finance = await this.financeService.create(request.body)
       return reply.code(201).send(finance)
     } catch (error) {
+      this.app.log.error(error, '创建财务记录失败')
       return reply.code(500).send({ message: '创建财务记录失败' })
     }
   }

+ 18 - 0
src/controllers/sys-config.controller.ts

@@ -181,4 +181,22 @@ export class SysConfigController {
       return reply.code(500).send({ message: '操作失败' })
     }
   }
+
+  /**
+   * 获取域名列表接口
+   * 从 sysconfig 读取 domain_redirect 和 domain_land 配置,返回列表
+   */
+  async getDomainLists(
+    request: FastifyRequest<{ Querystring: { teamId?: string } }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { teamId } = request.query
+      const teamIdNumber = teamId ? Number(teamId) : undefined
+      const result = await this.sysConfigService.getDomainLists(teamIdNumber)
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send({ message: '操作失败' })
+    }
+  }
 }

+ 160 - 7
src/controllers/team-members.controller.ts

@@ -11,10 +11,14 @@ import {
   TeamLeaderStatsQuery,
   TeamLeaderStatsResponse,
   PromotionLinkResponse,
-  GeneratePromoCodeResponse
+  GeneratePromoCodeResponse,
+  UpdateWithdrawPasswordBody,
+  UpdateWithdrawPasswordResponse,
+  WithdrawPasswordStatusResponse
 } from '../dto/team-members.dto'
 import { UserRole } from '../entities/user.entity'
 import { TeamService } from '../services/team.service'
+import { instanceToPlain } from 'class-transformer'
 
 export class TeamMembersController {
   private teamMembersService: TeamMembersService
@@ -36,8 +40,8 @@ export class TeamMembersController {
         request.body.teamId = team.id
         request.body.teamUserId = user.id
       }
-      const teamMember = await this.teamMembersService.create(request.body, request.user.id)
-      return reply.code(201).send(teamMember)
+      const teamMember = await this.teamMembersService.create(request.body, user.id)
+      return reply.code(201).send(instanceToPlain(teamMember))
     } catch (error) {
       console.error('创建团队成员失败:', error)
       const errorMessage = error instanceof Error ? error.message : '未知错误'
@@ -64,7 +68,7 @@ export class TeamMembersController {
     try {
       const { id } = request.params
       const teamMember = await this.teamMembersService.findById(id)
-      return reply.send(teamMember)
+      return reply.send(instanceToPlain(teamMember))
     } catch (error) {
       return reply.code(404).send({ message: '团队成员不存在' })
     }
@@ -84,7 +88,20 @@ export class TeamMembersController {
         request.query.userId = user.id
       }
       const result = await this.teamMembersService.findAll(request.query)
-      return reply.send(result)
+      // 处理树状结构中的团队成员数据
+      const processTree = (nodes: any[]): any[] => {
+        return nodes.map(node => {
+          const processedNode = instanceToPlain(node)
+          if (node.children && node.children.length > 0) {
+            processedNode.children = processTree(node.children)
+          }
+          return processedNode
+        })
+      }
+      return reply.send({
+        ...result,
+        content: processTree(result.content)
+      })
     } catch (error) {
       return reply.code(500).send({ message: '获取团队成员列表失败' })
     }
@@ -116,7 +133,7 @@ export class TeamMembersController {
       }
 
       const updatedMember = await this.teamMembersService.update(updateData)
-      return reply.send(updatedMember)
+      return reply.send(instanceToPlain(updatedMember))
     } catch (error) {
       console.error('更新团队成员失败:', error)
       const errorMessage = error instanceof Error ? error.message : '未知错误'
@@ -167,7 +184,7 @@ export class TeamMembersController {
       }
 
       const updatedMember = await this.teamMembersService.updateRevenue(id, amount, type)
-      return reply.send(updatedMember)
+      return reply.send(instanceToPlain(updatedMember))
     } catch (error) {
       return reply.code(500).send({ message: '更新团队成员收入失败' })
     }
@@ -653,4 +670,140 @@ export class TeamMembersController {
       return reply.code(500).send({ message: '获取每日统计数据失败' })
     }
   }
+
+  /**
+   * 修改团队成员提现密码
+   * 推广员修改自己的提现密码,团队管理员和管理员可以通过路径参数指定团队成员ID
+   */
+  async updateWithdrawPassword(request: FastifyRequest<{ Params?: TeamMembersParams; Body: UpdateWithdrawPasswordBody }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      const { oldPassword, newPassword } = request.body
+
+      if (!newPassword) {
+        return reply.code(400).send({ message: '新密码不能为空' })
+      }
+
+      let teamMemberId: number
+
+      // 权限检查:推广员只能修改自己的提现密码,团队管理员可以修改自己团队成员的提现密码
+      if (user.role === UserRole.PROMOTER) {
+        // 推广员只能修改自己的提现密码
+        const teamMember = await this.teamMembersService.findByUserId(user.id)
+        teamMemberId = teamMember.id
+      } else if (user.role === UserRole.TEAM) {
+        // 团队管理员可以通过路径参数指定团队成员ID
+        if (request.params && request.params.id) {
+          teamMemberId = Number(request.params.id)
+          
+          // 验证团队成员是否属于该团队
+          const teamMember = await this.teamMembersService.findById(teamMemberId)
+          const team = await this.teamService.findByUserId(user.id)
+          if (teamMember.teamId !== team.id) {
+            return reply.code(403).send({ message: '无权限修改其他团队的成员提现密码' })
+          }
+        } else {
+          return reply.code(400).send({ message: '请指定团队成员ID' })
+        }
+      } else if (user.role === UserRole.ADMIN) {
+        // 管理员可以通过路径参数指定团队成员ID
+        if (request.params && request.params.id) {
+          teamMemberId = Number(request.params.id)
+        } else {
+          return reply.code(400).send({ message: '请指定团队成员ID' })
+        }
+      } else {
+        return reply.code(403).send({ message: '无权限修改提现密码' })
+      }
+
+      await this.teamMembersService.updateWithdrawPassword(teamMemberId, oldPassword, newPassword)
+
+      const response: UpdateWithdrawPasswordResponse = {
+        message: '提现密码修改成功'
+      }
+
+      return reply.send(response)
+    } catch (error) {
+      console.error('修改提现密码失败:', error)
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      
+      if (errorMessage.includes('旧密码错误') || errorMessage.includes('请提供旧密码')) {
+        return reply.code(400).send({ 
+          message: errorMessage,
+          error: 'VALIDATION_ERROR'
+        })
+      }
+      
+      return reply.code(500).send({ 
+        message: '修改提现密码失败',
+        error: errorMessage
+      })
+    }
+  }
+
+  /**
+   * 检查团队成员是否已设置提现密码
+   * 推广员检查自己的,团队管理员和管理员可以指定团队成员ID
+   */
+  async checkWithdrawPasswordStatus(request: FastifyRequest<{ Params?: TeamMembersParams }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      let teamMemberId: number
+
+      // 权限检查:推广员只能检查自己的提现密码状态,团队管理员可以检查自己团队成员的提现密码状态
+      if (user.role === UserRole.PROMOTER) {
+        // 推广员只能检查自己的提现密码状态
+        const teamMember = await this.teamMembersService.findByUserId(user.id)
+        teamMemberId = teamMember.id
+      } else if (user.role === UserRole.TEAM) {
+        // 团队管理员可以通过路径参数指定团队成员ID
+        if (request.params && request.params.id) {
+          teamMemberId = Number(request.params.id)
+          
+          // 验证团队成员是否属于该团队
+          const teamMember = await this.teamMembersService.findById(teamMemberId)
+          const team = await this.teamService.findByUserId(user.id)
+          if (teamMember.teamId !== team.id) {
+            return reply.code(403).send({ message: '无权限检查其他团队的成员提现密码状态' })
+          }
+        } else {
+          return reply.code(400).send({ message: '请指定团队成员ID' })
+        }
+      } else if (user.role === UserRole.ADMIN) {
+        // 管理员可以通过路径参数指定团队成员ID
+        if (request.params && request.params.id) {
+          teamMemberId = Number(request.params.id)
+        } else {
+          return reply.code(400).send({ message: '请指定团队成员ID' })
+        }
+      } else {
+        return reply.code(403).send({ message: '无权限检查提现密码状态' })
+      }
+
+      const hasPassword = await this.teamMembersService.checkWithdrawPasswordStatus(teamMemberId)
+
+      const response: WithdrawPasswordStatusResponse = {
+        hasPassword,
+        message: hasPassword ? '已设置提现密码' : '未设置提现密码'
+      }
+
+      return reply.send(response)
+    } catch (error) {
+      console.error('检查提现密码状态失败:', error)
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      
+      return reply.code(500).send({ 
+        message: '检查提现密码状态失败',
+        error: errorMessage
+      })
+    }
+  }
 }

+ 184 - 9
src/controllers/team.controller.ts

@@ -1,7 +1,8 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
 import { TeamService } from '../services/team.service'
-import { CreateTeamBody, UpdateTeamBody, ListTeamQuery, TeamParams, UpdateRevenueBody, UpdateThemeColorBody } from '../dto/team.dto'
+import { CreateTeamBody, UpdateTeamBody, ListTeamQuery, TeamParams, UpdateRevenueBody, UpdateThemeColorBody, GenerateFirstLevelAgentLinkResponse, UpdateWithdrawPasswordBody, UpdateWithdrawPasswordResponse, WithdrawPasswordStatusResponse } from '../dto/team.dto'
 import { UserRole } from '../entities/user.entity'
+import { instanceToPlain } from 'class-transformer'
 
 export class TeamController {
   private teamService: TeamService
@@ -13,7 +14,7 @@ export class TeamController {
   async create(request: FastifyRequest<{ Body: CreateTeamBody }>, reply: FastifyReply) {
     try {
       const team = await this.teamService.create(request.body, request.user.id)
-      return reply.code(201).send(team)
+      return reply.code(201).send(instanceToPlain(team))
     } catch (error) {
       return reply.code(500).send({ message: '创建团队失败' })
     }
@@ -23,7 +24,7 @@ export class TeamController {
     try {
       const { id } = request.params
       const team = await this.teamService.findById(id)
-      return reply.send(team)
+      return reply.send(instanceToPlain(team))
     } catch (error) {
       return reply.code(404).send({ message: '团队不存在' })
     }
@@ -39,7 +40,10 @@ export class TeamController {
         request.query.userId = user.id
       }
       const result = await this.teamService.findAll(request.query)
-      return reply.send(result)
+      return reply.send({
+        ...result,
+        content: result.content.map(team => instanceToPlain(team))
+      })
     } catch (error) {
       return reply.code(500).send({ message: '获取团队列表失败' })
     }
@@ -57,7 +61,7 @@ export class TeamController {
       }
 
       const updatedTeam = await this.teamService.update(updateData)
-      return reply.send(updatedTeam)
+      return reply.send(instanceToPlain(updatedTeam))
     } catch (error) {
       return reply.code(500).send({ message: '更新团队失败' })
     }
@@ -91,7 +95,7 @@ export class TeamController {
       }
 
       const updatedTeam = await this.teamService.updateRevenue(id, amount, type)
-      return reply.send(updatedTeam)
+      return reply.send(instanceToPlain(updatedTeam))
     } catch (error) {
       return reply.code(500).send({ message: '更新团队收入失败' })
     }
@@ -130,7 +134,7 @@ export class TeamController {
   async getTeams(request: FastifyRequest, reply: FastifyReply) {
     try {
       const teams = await this.teamService.getTeams()
-      return reply.send(teams)
+      return reply.send(teams.map(team => instanceToPlain(team)))
     } catch (error) {
       return reply.code(500).send({ message: '获取团队失败' })
     }
@@ -143,7 +147,7 @@ export class TeamController {
         return reply.code(403).send({ message: '用户未登录' })
       }
       const team = await this.teamService.findByUserId(user.id)
-      return reply.send(team)
+      return reply.send(instanceToPlain(team))
     } catch (error) {
       return reply.code(404).send({ message: '团队不存在' })
     }
@@ -177,7 +181,7 @@ export class TeamController {
         return reply.code(403).send({ message: '只有团队用户和管理员可以使用此接口' })
       }
       const updatedTeam = await this.teamService.updateThemeColor(user.id, request.body.themeColor)
-      return reply.send(updatedTeam)
+      return reply.send(instanceToPlain(updatedTeam))
     } catch (error) {
       const errorMessage = error instanceof Error ? error.message : '更新主题颜色失败'
       return reply.code(500).send({ message: errorMessage })
@@ -203,4 +207,175 @@ export class TeamController {
       return reply.code(500).send({ message: '获取团队主题颜色失败' })
     }
   }
+
+  /**
+   * 生成一级代理链接(不带code,直接域名跳转)
+   * 一级代理就是团队(team)
+   */
+  async generateFirstLevelAgentLink(request: FastifyRequest<{ Params: TeamParams }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      const { id } = request.params
+
+      // 权限检查:只有团队管理员或系统管理员可以生成一级代理链接
+      if (user.role === UserRole.TEAM) {
+        // 团队管理员只能为自己的团队生成一级代理链接
+        const team = await this.teamService.findByUserId(user.id)
+        if (team.id !== Number(id)) {
+          return reply.code(403).send({ message: '无权限为其他团队生成一级代理链接' })
+        }
+      } else if (user.role !== UserRole.ADMIN) {
+        return reply.code(403).send({ message: '无权限生成一级代理链接' })
+      }
+
+      // 生成一级代理链接
+      const links = await this.teamService.generateFirstLevelAgentLink(Number(id))
+      const team = await this.teamService.findById(Number(id))
+
+      const response: GenerateFirstLevelAgentLinkResponse = {
+        teamId: team.id,
+        generalLink: links.generalLink,
+        browserLink: links.browserLink,
+        message: '一级代理链接生成成功'
+      }
+
+      return reply.send(response)
+    } catch (error) {
+      console.error('生成一级代理链接失败:', error)
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      
+      // 如果是找不到记录的错误,返回404
+      if (errorMessage.includes('Could not find any entity') || errorMessage.includes('未找到')) {
+        return reply.code(404).send({ 
+          message: '团队不存在',
+          error: errorMessage
+        })
+      }
+      
+      // 如果未配置域名
+      if (errorMessage.includes('未配置域名')) {
+        return reply.code(400).send({ 
+          message: errorMessage,
+          error: 'CONFIG_ERROR'
+        })
+      }
+      
+      return reply.code(500).send({ 
+        message: '生成一级代理链接失败',
+        error: errorMessage
+      })
+    }
+  }
+
+  /**
+   * 修改团队提现密码
+   * 团队用户修改自己的提现密码,管理员可以通过路径参数指定团队ID
+   */
+  async updateWithdrawPassword(request: FastifyRequest<{ Params?: TeamParams; Body: UpdateWithdrawPasswordBody }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      const { oldPassword, newPassword } = request.body
+
+      if (!newPassword) {
+        return reply.code(400).send({ message: '新密码不能为空' })
+      }
+
+      let teamId: number
+
+      // 权限检查:只有团队用户和管理员可以修改提现密码
+      if (user.role === UserRole.TEAM) {
+        // 团队用户只能修改自己团队的提现密码
+        const team = await this.teamService.findByUserId(user.id)
+        teamId = team.id
+      } else if (user.role === UserRole.ADMIN) {
+        // 管理员可以通过路径参数指定团队ID,如果没有指定则返回错误
+        if (request.params && request.params.id) {
+          teamId = Number(request.params.id)
+        } else {
+          return reply.code(400).send({ message: '管理员需要指定团队ID' })
+        }
+      } else {
+        return reply.code(403).send({ message: '无权限修改提现密码' })
+      }
+
+      await this.teamService.updateWithdrawPassword(teamId, oldPassword, newPassword)
+
+      const response: UpdateWithdrawPasswordResponse = {
+        message: '提现密码修改成功'
+      }
+
+      return reply.send(response)
+    } catch (error) {
+      console.error('修改提现密码失败:', error)
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      
+      if (errorMessage.includes('旧密码错误') || errorMessage.includes('请提供旧密码')) {
+        return reply.code(400).send({ 
+          message: errorMessage,
+          error: 'VALIDATION_ERROR'
+        })
+      }
+      
+      return reply.code(500).send({ 
+        message: '修改提现密码失败',
+        error: errorMessage
+      })
+    }
+  }
+
+  /**
+   * 检查团队是否已设置提现密码
+   * 团队用户检查自己的,管理员可以指定团队ID
+   */
+  async checkWithdrawPasswordStatus(request: FastifyRequest<{ Params?: TeamParams }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      let teamId: number
+
+      // 权限检查:只有团队用户和管理员可以检查提现密码状态
+      if (user.role === UserRole.TEAM) {
+        // 团队用户只能检查自己团队的提现密码状态
+        const team = await this.teamService.findByUserId(user.id)
+        teamId = team.id
+      } else if (user.role === UserRole.ADMIN) {
+        // 管理员可以通过路径参数指定团队ID,如果没有指定则返回错误
+        if (request.params && request.params.id) {
+          teamId = Number(request.params.id)
+        } else {
+          return reply.code(400).send({ message: '管理员需要指定团队ID' })
+        }
+      } else {
+        return reply.code(403).send({ message: '无权限检查提现密码状态' })
+      }
+
+      const hasPassword = await this.teamService.checkWithdrawPasswordStatus(teamId)
+
+      const response: WithdrawPasswordStatusResponse = {
+        hasPassword,
+        message: hasPassword ? '已设置提现密码' : '未设置提现密码'
+      }
+
+      return reply.send(response)
+    } catch (error) {
+      console.error('检查提现密码状态失败:', error)
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      
+      return reply.code(500).send({ 
+        message: '检查提现密码状态失败',
+        error: errorMessage
+      })
+    }
+  }
 }

+ 128 - 0
src/controllers/video-feedback.controller.ts

@@ -0,0 +1,128 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { VideoFeedbackService } from '../services/video-feedback.service'
+import {
+  CreateVideoFeedbackBody,
+  CreateVideoFeedbackResponse,
+  ListVideoFeedbackQuery,
+  ListVideoFeedbackResponse
+} from '../dto/video-feedback.dto'
+
+export class VideoFeedbackController {
+  private feedbackService: VideoFeedbackService
+  private app: FastifyInstance
+
+  constructor(app: FastifyInstance) {
+    this.app = app
+    this.feedbackService = new VideoFeedbackService(app)
+  }
+
+  /**
+   * 统一错误响应格式
+   */
+  private createErrorResponse(msg: string, data: any = null): CreateVideoFeedbackResponse | ListVideoFeedbackResponse {
+    return {
+      code: 0,
+      msg,
+      time: Math.floor(Date.now() / 1000).toString(),
+      data
+    }
+  }
+
+  /**
+   * 统一成功响应格式(提交反馈)
+   */
+  private createSuccessResponse(msg: string = '操作成功'): CreateVideoFeedbackResponse {
+    return {
+      code: 1,
+      msg,
+      time: Math.floor(Date.now() / 1000).toString(),
+      data: {
+        success: true
+      }
+    }
+  }
+
+  /**
+   * 提交视频反馈
+   * 需要用户登录
+   */
+  async submitFeedback(
+    request: FastifyRequest<{ Body: CreateVideoFeedbackBody }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { videoId, reason, url } = request.body
+      const userId = request.user?.id
+
+      if (!userId) {
+        return reply.code(401).send(
+          this.createErrorResponse('用户未登录')
+        )
+      }
+
+      if (!videoId) {
+        return reply.code(400).send(
+          this.createErrorResponse('视频ID参数必填')
+        )
+      }
+
+      if (!reason || reason.trim() === '') {
+        return reply.code(400).send(
+          this.createErrorResponse('反馈理由不能为空')
+        )
+      }
+
+      await this.feedbackService.submitFeedback(videoId, userId, reason, url)
+
+      return reply.send(
+        this.createSuccessResponse('反馈提交成功')
+      )
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '提交反馈失败'
+      this.app.log.error(`提交视频反馈失败: ${errorMessage}`)
+
+      return reply.code(500).send(
+        this.createErrorResponse(errorMessage)
+      )
+    }
+  }
+
+  /**
+   * 获取反馈列表(分页)
+   */
+  async getFeedbackList(
+    request: FastifyRequest<{ Querystring: ListVideoFeedbackQuery }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const { page = 0, size = 20 } = request.query
+
+      const result = await this.feedbackService.getFeedbackList(page, size)
+
+      return reply.send({
+        code: 1,
+        msg: '获取成功',
+        time: Math.floor(Date.now() / 1000).toString(),
+        data: {
+          list: result.content,
+          total: result.metadata.total,
+          page: result.metadata.page,
+          size: result.metadata.size
+        }
+      } as ListVideoFeedbackResponse)
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '获取反馈列表失败'
+      this.app.log.error(`获取反馈列表失败: ${errorMessage}`)
+
+      return reply.code(500).send(
+        this.createErrorResponse(errorMessage, {
+          list: [],
+          total: 0,
+          page: request.query.page || 0,
+          size: request.query.size || 20
+        })
+      )
+    }
+  }
+}
+

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

@@ -6,6 +6,7 @@ export interface CreateFinanceBody {
   teamId: number
   userId: number
   reminderAmount: number
+  withdrawPassword?: string
   paymentQrCode?: string
   paymentName?: string
   paymentAccount?: string

+ 1 - 1
src/dto/promotion-link.dto.ts

@@ -4,7 +4,7 @@ import { Pagination } from './common.dto'
 
 export interface CreatePromotionLinkBody {
   teamId?: number
-  memberId?: number
+  memberId?: number | null
   name: string
   image: string
   link: string

+ 14 - 0
src/dto/team-members.dto.ts

@@ -142,4 +142,18 @@ export interface TeamMemberStatsTreeNode {
   // 树状结构
   type: 'team' | 'teamMember' | 'other'
   children: TeamMemberStatsTreeNode[]
+}
+
+export interface UpdateWithdrawPasswordBody {
+  oldPassword?: string
+  newPassword: string
+}
+
+export interface UpdateWithdrawPasswordResponse {
+  message: string
+}
+
+export interface WithdrawPasswordStatusResponse {
+  hasPassword: boolean
+  message: string
 }

+ 21 - 0
src/dto/team.dto.ts

@@ -46,3 +46,24 @@ export interface UpdateRevenueBody {
 export interface UpdateThemeColorBody {
   themeColor: string
 }
+
+export interface GenerateFirstLevelAgentLinkResponse {
+  teamId: number
+  generalLink: string
+  browserLink: string
+  message: string
+}
+
+export interface UpdateWithdrawPasswordBody {
+  oldPassword?: string
+  newPassword: string
+}
+
+export interface UpdateWithdrawPasswordResponse {
+  message: string
+}
+
+export interface WithdrawPasswordStatusResponse {
+  hasPassword: boolean
+  message: string
+}

+ 80 - 0
src/dto/video-feedback.dto.ts

@@ -0,0 +1,80 @@
+import { Pagination } from './common.dto'
+
+/**
+ * 提交视频反馈请求体
+ */
+export interface CreateVideoFeedbackBody {
+  /** 视频ID(必填) */
+  videoId: number
+  /** 反馈理由(必填) */
+  reason: string
+  /** 用户访问的视频链接(可选) */
+  url?: string
+}
+
+/**
+ * 视频反馈列表查询参数
+ */
+export interface ListVideoFeedbackQuery extends Pagination {
+  /** 页码,默认为0 */
+  page?: number
+  /** 每页数量,默认为20 */
+  size?: number
+}
+
+/**
+ * 视频反馈数据结构
+ */
+export interface VideoFeedback {
+  /** 视频ID */
+  videoId: number
+  /** 用户ID */
+  userId: number
+  /** 反馈理由 */
+  reason: string
+  /** 反馈时间戳(秒) */
+  timestamp: number
+  /** 当前访问链接 */
+  url?: string
+}
+
+/**
+ * 提交反馈响应数据结构
+ */
+export interface CreateVideoFeedbackResponse {
+  /** 响应码,0表示失败,1表示成功 */
+  code: number
+  /** 响应消息 */
+  msg: string
+  /** 时间戳(秒) */
+  time: string
+  /** 响应数据 */
+  data: {
+    /** 是否成功 */
+    success: boolean
+  } | null
+}
+
+/**
+ * 反馈列表响应数据结构
+ */
+export interface ListVideoFeedbackResponse {
+  /** 响应码,0表示失败,1表示成功 */
+  code: number
+  /** 响应消息 */
+  msg: string
+  /** 时间戳(秒) */
+  time: string
+  /** 响应数据 */
+  data: {
+    /** 反馈列表 */
+    list: VideoFeedback[]
+    /** 总记录数 */
+    total: number
+    /** 当前页码 */
+    page: number
+    /** 每页数量 */
+    size: number
+  } | null
+}
+

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

@@ -15,7 +15,7 @@ export class PromotionLink {
   teamId: number
 
   @Column({ type: 'int', nullable: true })
-  memberId: number
+  memberId: number | null
 
   @Column()
   name: string

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

@@ -1,4 +1,5 @@
 import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+import { Exclude } from 'class-transformer'
 
 @Entity()
 export class TeamMembers {
@@ -29,6 +30,10 @@ export class TeamMembers {
   @Column({ type: 'int', nullable: true })
   parentId: number | null // 父级团队成员的 id(team_members 表的 id)
 
+  @Exclude()
+  @Column({ type: 'varchar', length: 255, nullable: true })
+  withdrawPassword: string | null
+
   @CreateDateColumn()
   createdAt: Date
 

+ 5 - 0
src/entities/team.entity.ts

@@ -1,4 +1,5 @@
 import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+import { Exclude } from 'class-transformer'
 
 @Entity()
 export class Team {
@@ -38,6 +39,10 @@ export class Team {
   @Column({ length: 20, nullable: true, default: 'dark' })
   themeColor: string
 
+  @Exclude()
+  @Column({ type: 'varchar', length: 255, nullable: true })
+  withdrawPassword: string | null
+
   @CreateDateColumn()
   createdAt: Date
 

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

@@ -11,7 +11,7 @@ export default async function financeRoutes(fastify: FastifyInstance) {
   // 创建财务记录
   fastify.post<{ Body: CreateFinanceBody }>(
     '/',
-    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
     financeController.create.bind(financeController)
   )
 

+ 6 - 0
src/routes/sys-config.routes.ts

@@ -74,4 +74,10 @@ export default async function sysConfigRoutes(fastify: FastifyInstance) {
   )
 
   fastify.get('/team/user', { onRequest: [authenticate] }, sysConfigController.getUserTeamConfigs.bind(sysConfigController))
+
+  // 获取域名列表接口
+  fastify.get<{ Querystring: { teamId?: string } }>(
+    '/domains',
+    sysConfigController.getDomainLists.bind(sysConfigController)
+  )
 }

+ 30 - 1
src/routes/team-members.routes.ts

@@ -9,7 +9,8 @@ import {
   TeamMembersParams,
   UpdateRevenueBody,
   TeamMemberStatsQuery,
-  TeamLeaderStatsQuery
+  TeamLeaderStatsQuery,
+  UpdateWithdrawPasswordBody
 } from '../dto/team-members.dto'
 
 export default async function teamMembersRoutes(fastify: FastifyInstance) {
@@ -133,4 +134,32 @@ export default async function teamMembersRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate, hasAnyRole(UserRole.TEAM, UserRole.PROMOTER)] },
     teamMembersController.getDailyStatisticsByPersonalAgentId.bind(teamMembersController)
   )
+
+  // 检查团队成员是否已设置提现密码(推广员检查自己的,团队管理员和管理员可以指定团队成员ID)
+  fastify.get<{}>(
+    '/withdraw-password/status',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM, UserRole.PROMOTER)] },
+    teamMembersController.checkWithdrawPasswordStatus.bind(teamMembersController)
+  )
+
+  // 团队管理员和管理员检查指定团队成员的提现密码状态
+  fastify.get<{ Params: TeamMembersParams }>(
+    '/:id/withdraw-password/status',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    teamMembersController.checkWithdrawPasswordStatus.bind(teamMembersController)
+  )
+
+  // 修改团队成员提现密码(推广员修改自己的,团队管理员和管理员可以指定团队成员ID)
+  fastify.put<{ Body: UpdateWithdrawPasswordBody }>(
+    '/withdraw-password',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM, UserRole.PROMOTER)] },
+    teamMembersController.updateWithdrawPassword.bind(teamMembersController)
+  )
+
+  // 团队管理员和管理员修改指定团队成员的提现密码
+  fastify.put<{ Params: TeamMembersParams; Body: UpdateWithdrawPasswordBody }>(
+    '/:id/withdraw-password',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    teamMembersController.updateWithdrawPassword.bind(teamMembersController)
+  )
 }

+ 36 - 1
src/routes/team.routes.ts

@@ -2,7 +2,7 @@ import { FastifyInstance } from 'fastify'
 import { TeamController } from '../controllers/team.controller'
 import { authenticate, hasAnyRole, hasRole } from '../middlewares/auth.middleware'
 import { UserRole } from '../entities/user.entity'
-import { CreateTeamBody, UpdateTeamBody, ListTeamQuery, TeamParams, UpdateRevenueBody, UpdateThemeColorBody } from '../dto/team.dto'
+import { CreateTeamBody, UpdateTeamBody, ListTeamQuery, TeamParams, UpdateRevenueBody, UpdateThemeColorBody, UpdateWithdrawPasswordBody } from '../dto/team.dto'
 
 export default async function teamRoutes(fastify: FastifyInstance) {
   const teamController = new TeamController(fastify)
@@ -96,4 +96,39 @@ export default async function teamRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate] },
     teamController.getMyTeamTheme.bind(teamController)
   )
+
+  // 生成一级代理链接(不带code,直接域名跳转)
+  fastify.post<{ Params: TeamParams }>(
+    '/:id/generate-first-level-agent-link',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    teamController.generateFirstLevelAgentLink.bind(teamController)
+  )
+
+  // 检查团队是否已设置提现密码(团队用户检查自己的,管理员可以指定团队ID)
+  fastify.get<{}>(
+    '/withdraw-password/status',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    teamController.checkWithdrawPasswordStatus.bind(teamController)
+  )
+
+  // 管理员检查指定团队的提现密码状态
+  fastify.get<{ Params: TeamParams }>(
+    '/:id/withdraw-password/status',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    teamController.checkWithdrawPasswordStatus.bind(teamController)
+  )
+
+  // 修改团队提现密码(团队用户修改自己的,管理员可以指定团队ID)
+  fastify.put<{ Body: UpdateWithdrawPasswordBody }>(
+    '/withdraw-password',
+    { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM)] },
+    teamController.updateWithdrawPassword.bind(teamController)
+  )
+
+  // 管理员修改指定团队的提现密码
+  fastify.put<{ Params: TeamParams; Body: UpdateWithdrawPasswordBody }>(
+    '/:id/withdraw-password',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    teamController.updateWithdrawPassword.bind(teamController)
+  )
 }

+ 39 - 0
src/routes/video-feedback.routes.ts

@@ -0,0 +1,39 @@
+import { FastifyInstance } from 'fastify'
+import { VideoFeedbackController } from '../controllers/video-feedback.controller'
+import { authenticate } from '../middlewares/auth.middleware'
+import { CreateVideoFeedbackBody, ListVideoFeedbackQuery } from '../dto/video-feedback.dto'
+
+/**
+ * 视频反馈接口路由
+ * 前缀: /api/video-feedback
+ */
+export default async function videoFeedbackRoutes(fastify: FastifyInstance) {
+  const feedbackController = new VideoFeedbackController(fastify)
+
+  /**
+   * POST /api/video-feedback/submit
+   * 提交视频反馈
+   * 请求体:
+   *   - videoId: 视频ID(必填)
+   *   - reason: 反馈理由(必填)
+   * 需要登录
+   */
+  fastify.post<{ Body: CreateVideoFeedbackBody }>(
+    '/submit',
+    { onRequest: [authenticate] },
+    feedbackController.submitFeedback.bind(feedbackController)
+  )
+
+  /**
+   * GET /api/video-feedback/list
+   * 获取反馈列表(分页)
+   * 查询参数:
+   *   - page: 页码(可选,默认0)
+   *   - size: 每页数量(可选,默认20)
+   */
+  fastify.get<{ Querystring: ListVideoFeedbackQuery }>(
+    '/list',
+    feedbackController.getFeedbackList.bind(feedbackController)
+  )
+}
+

+ 47 - 5
src/services/promotion-link.service.ts

@@ -1,4 +1,4 @@
-import { Repository, Like } from 'typeorm'
+import { Repository, Like, IsNull } from 'typeorm'
 import { FastifyInstance } from 'fastify'
 import { PromotionLink, LinkType } from '../entities/promotion-link.entity'
 import { PaginationResponse } from '../dto/common.dto'
@@ -36,8 +36,21 @@ export class PromotionLinkService {
   /**
    * 根据 memberId + type 查找推广链接
    */
-  async findByMemberIdAndType(memberId: number, type: LinkType): Promise<PromotionLink | null> {
-    return this.promotionLinkRepository.findOne({ where: { memberId, type } })
+  async findByMemberIdAndType(memberId: number | null, type: LinkType): Promise<PromotionLink | null> {
+    const where: any = { type }
+    if (memberId === null) {
+      where.memberId = IsNull()
+    } else {
+      where.memberId = memberId
+    }
+    return this.promotionLinkRepository.findOne({ where })
+  }
+
+  /**
+   * 根据 teamId + memberId(null) + type 查找推广链接(用于团队的一级代理链接)
+   */
+  async findByTeamIdAndMemberIdAndType(teamId: number, memberId: null, type: LinkType): Promise<PromotionLink | null> {
+    return this.promotionLinkRepository.findOne({ where: { teamId, memberId: IsNull(), type } })
   }
 
   /**
@@ -75,13 +88,42 @@ export class PromotionLinkService {
   /**
    * 创建或更新推广链接(根据 memberId + type)
    * 用于同一个成员生成多种类型的链接(如通用、浏览器)
+   * 如果 memberId 为 null,则根据 teamId + memberId(null) + type 查找
    */
   async createOrUpdateByMemberIdAndType(data: CreatePromotionLinkBody): Promise<PromotionLink> {
-    if (!data.memberId) {
+    const linkType = data.type ?? LinkType.GENERAL
+    
+    // 如果 memberId 为 null,需要根据 teamId + memberId(null) + type 查找
+    if (data.memberId === null || data.memberId === undefined) {
+      if (!data.teamId) {
+        // 如果没有 teamId,直接创建
+        return this.create(data)
+      }
+      
+      const existingLink = await this.promotionLinkRepository.findOne({ 
+        where: { 
+          teamId: data.teamId, 
+          memberId: IsNull(), 
+          type: linkType 
+        } 
+      })
+
+      if (existingLink) {
+        const updateData: any = {}
+        if (data.name !== undefined) updateData.name = data.name
+        if (data.image !== undefined) updateData.image = data.image
+        if (data.link !== undefined) updateData.link = data.link
+        if (data.type !== undefined) updateData.type = data.type
+
+        await this.promotionLinkRepository.update(existingLink.id, updateData)
+        return this.findById(existingLink.id)
+      }
+
       return this.create(data)
     }
 
-    const existingLink = await this.findByMemberIdAndType(data.memberId, data.type ?? LinkType.GENERAL)
+    // memberId 不为 null 的情况,按原来的逻辑处理
+    const existingLink = await this.findByMemberIdAndType(data.memberId, linkType)
 
     if (existingLink) {
       const updateData: any = {}

+ 97 - 0
src/services/sys-config.service.ts

@@ -90,6 +90,103 @@ export class SysConfigService {
     }
   }
 
+  /**
+   * 从 sysconfig 读取 domain_redirect 和 domain_land 配置
+   * 将逗号分隔的字符串转换为列表返回
+   * @param teamId 可选的团队ID,如果不提供则读取全局配置(teamId=0),如果为0则返回所有团队的配置(不筛选)
+   * @returns 包含 domainRedirect 和 domainLand 两个列表的对象
+   */
+  async getDomainLists(teamId?: number): Promise<{ domainRedirect: string[]; domainLand: string[] }> {
+    const targetTeamId = teamId !== undefined ? teamId : 0
+    
+    // 读取 domain_redirect 配置
+    let domainRedirect: string[] = []
+    try {
+      if (targetTeamId === 0) {
+        // teamId 为 0 时,查询所有团队的配置并合并
+        const redirectConfigs = await this.sysConfigRepository.find({ 
+          where: { name: 'domain_redirect' } 
+        })
+        this.app.log.info(`查询 domain_redirect 配置,找到 ${redirectConfigs.length} 条记录`)
+        const allDomains = new Set<string>()
+        redirectConfigs.forEach(config => {
+          this.app.log.info(`处理配置: teamId=${config.teamId}, name=${config.name}, value=${config.value}`)
+          if (config && config.value) {
+            const domains = config.value
+              .split(',')
+              .map(item => item.trim())
+              .filter(item => item.length > 0)
+            this.app.log.info(`解析出域名: ${domains.join(', ')}`)
+            domains.forEach(domain => allDomains.add(domain))
+          }
+        })
+        domainRedirect = Array.from(allDomains)
+        this.app.log.info(`最终 domainRedirect 列表: ${domainRedirect.join(', ')}`)
+      } else {
+        // 查询指定团队的配置
+        const redirectConfig = await this.sysConfigRepository.findOne({ 
+          where: { name: 'domain_redirect', teamId: targetTeamId } 
+        })
+        this.app.log.info(`查询指定团队配置: teamId=${targetTeamId}, 找到配置: ${redirectConfig ? '是' : '否'}`)
+        if (redirectConfig && redirectConfig.value) {
+          this.app.log.info(`配置值: ${redirectConfig.value}`)
+          domainRedirect = redirectConfig.value
+            .split(',')
+            .map(item => item.trim())
+            .filter(item => item.length > 0)
+        }
+      }
+    } catch (e) {
+      this.app.log.error(e, '读取 domain_redirect 配置失败')
+    }
+
+    // 读取 domain_land 配置
+    let domainLand: string[] = []
+    try {
+      if (targetTeamId === 0) {
+        // teamId 为 0 时,查询所有团队的配置并合并
+        const landConfigs = await this.sysConfigRepository.find({ 
+          where: { name: 'domain_land' } 
+        })
+        this.app.log.info(`查询 domain_land 配置,找到 ${landConfigs.length} 条记录`)
+        const allDomains = new Set<string>()
+        landConfigs.forEach(config => {
+          this.app.log.info(`处理配置: teamId=${config.teamId}, name=${config.name}, value=${config.value}`)
+          if (config && config.value) {
+            const domains = config.value
+              .split(',')
+              .map(item => item.trim())
+              .filter(item => item.length > 0)
+            this.app.log.info(`解析出域名: ${domains.join(', ')}`)
+            domains.forEach(domain => allDomains.add(domain))
+          }
+        })
+        domainLand = Array.from(allDomains)
+        this.app.log.info(`最终 domainLand 列表: ${domainLand.join(', ')}`)
+      } else {
+        // 查询指定团队的配置
+        const landConfig = await this.sysConfigRepository.findOne({ 
+          where: { name: 'domain_land', teamId: targetTeamId } 
+        })
+        this.app.log.info(`查询指定团队配置: teamId=${targetTeamId}, 找到配置: ${landConfig ? '是' : '否'}`)
+        if (landConfig && landConfig.value) {
+          this.app.log.info(`配置值: ${landConfig.value}`)
+          domainLand = landConfig.value
+            .split(',')
+            .map(item => item.trim())
+            .filter(item => item.length > 0)
+        }
+      }
+    } catch (e) {
+      this.app.log.error(e, '读取 domain_land 配置失败')
+    }
+
+    return {
+      domainRedirect,
+      domainLand
+    }
+  }
+
   async create(data: CreateSysConfigBody) {
     const where: any = { name: data.name }
     if (data.teamId !== undefined) {

+ 45 - 2
src/services/team-members.service.ts

@@ -17,6 +17,7 @@ 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'
+import bcrypt from 'bcryptjs'
 
 export class TeamMembersService {
   private teamMembersRepository: Repository<TeamMembers>
@@ -1228,6 +1229,11 @@ export class TeamMembersService {
       finalDomain = `https://${finalDomain}`
     }
 
+    // 确保域名后面有 /,如果没有则添加
+    if (!finalDomain.endsWith('/')) {
+      finalDomain = `${finalDomain}/`
+    }
+
     // 生成推广链接:{domain}?code={推广码}
     const generalLink = `${finalDomain}?code=${teamMember.promoCode}`
     // 浏览器链接增加 redirect=1 参数
@@ -1239,7 +1245,7 @@ export class TeamMembersService {
       await this.promotionLinkService.createOrUpdateByMemberIdAndType({
         teamId: teamMember.teamId,
         memberId: teamMember.id,
-        name: `${teamMember.name}的推广链接`,
+        name: `${teamMember.name}的微信直开`,
         image: '',
         link: generalLink,
         type: LinkType.GENERAL
@@ -1247,7 +1253,7 @@ export class TeamMembersService {
       await this.promotionLinkService.createOrUpdateByMemberIdAndType({
         teamId: teamMember.teamId,
         memberId: teamMember.id,
-        name: `${teamMember.name}的浏览器推广链接`,
+        name: `${teamMember.name}的微信跳转(稳定)`,
         image: '',
         link: browserLink,
         type: LinkType.BROWSER
@@ -1582,4 +1588,41 @@ export class TeamMembersService {
       todaySales
     }
   }
+
+  /**
+   * 修改团队成员提现密码
+   * @param teamMemberId 团队成员ID
+   * @param oldPassword 旧密码(如果已设置过密码,需要提供旧密码)
+   * @param newPassword 新密码
+   */
+  async updateWithdrawPassword(teamMemberId: number, oldPassword: string | undefined, newPassword: string): Promise<void> {
+    const teamMember = await this.findById(teamMemberId)
+    
+    // 如果已设置过提现密码,需要验证旧密码
+    if (teamMember.withdrawPassword) {
+      if (!oldPassword) {
+        throw new Error('请提供旧密码')
+      }
+      const isPasswordValid = await bcrypt.compare(oldPassword, teamMember.withdrawPassword)
+      if (!isPasswordValid) {
+        throw new Error('旧密码错误')
+      }
+    }
+    
+    // 加密新密码
+    const hashedPassword = await bcrypt.hash(newPassword, 10)
+    
+    // 更新提现密码
+    await this.teamMembersRepository.update(teamMemberId, { withdrawPassword: hashedPassword })
+  }
+
+  /**
+   * 检查团队成员是否已设置提现密码
+   * @param teamMemberId 团队成员ID
+   * @returns 是否已设置提现密码
+   */
+  async checkWithdrawPasswordStatus(teamMemberId: number): Promise<boolean> {
+    const teamMember = await this.findById(teamMemberId)
+    return !!teamMember.withdrawPassword
+  }
 }

+ 172 - 0
src/services/team.service.ts

@@ -6,13 +6,18 @@ import { Member } from '../entities/member.entity'
 import { User } from '../entities/user.entity'
 import { TeamMembers } from '../entities/team-members.entity'
 import { TeamDomain } from '../entities/team-domain.entity'
+import { DomainManagement, DomainType as DomainManagementType } from '../entities/domain-management.entity'
+import { Finance, FinanceStatus } from '../entities/finance.entity'
 import { PaginationResponse } from '../dto/common.dto'
 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 { PromotionLinkService } from './promotion-link.service'
+import { LinkType } from '../entities/promotion-link.entity'
 import * as randomstring from 'randomstring'
+import bcrypt from 'bcryptjs'
 
 export class TeamService {
   private teamRepository: Repository<Team>
@@ -21,20 +26,28 @@ export class TeamService {
   private userRepository: Repository<User>
   private teamMembersRepository: Repository<TeamMembers>
   private teamDomainRepository: Repository<TeamDomain>
+  private domainManagementRepository: Repository<DomainManagement>
+  private financeRepository: Repository<Finance>
   private userService: UserService
   private sysConfigService: SysConfigService
   private multiLevelCommissionService: MultiLevelCommissionService
+  private promotionLinkService: PromotionLinkService
+  private app: FastifyInstance
 
   constructor(app: FastifyInstance) {
+    this.app = app
     this.teamRepository = app.dataSource.getRepository(Team)
     this.incomeRecordsRepository = app.dataSource.getRepository(IncomeRecords)
     this.memberRepository = app.dataSource.getRepository(Member)
     this.userRepository = app.dataSource.getRepository(User)
     this.teamMembersRepository = app.dataSource.getRepository(TeamMembers)
     this.teamDomainRepository = app.dataSource.getRepository(TeamDomain)
+    this.domainManagementRepository = app.dataSource.getRepository(DomainManagement)
+    this.financeRepository = app.dataSource.getRepository(Finance)
     this.userService = new UserService(app)
     this.sysConfigService = new SysConfigService(app)
     this.multiLevelCommissionService = new MultiLevelCommissionService(app)
+    this.promotionLinkService = new PromotionLinkService(app)
   }
 
   async create(data: CreateTeamBody, creatorId: number): Promise<Team> {
@@ -148,6 +161,7 @@ export class TeamService {
     todayDAU: number
     todayNewUsers: number
     averageCommissionRate: number
+    balance: number
     allTeams: Array<{ id: number; name: string; totalRevenue: number; todayRevenue: number; totalSales: number; todaySales: number; todayDAU: number; todayNewUsers: number }>
   }> {
     // 根据 userId 参数决定查询范围
@@ -308,6 +322,24 @@ export class TeamService {
       totalUsersMap.set(stat.teamId, Number(stat.totalUsers) || 0)
     })
 
+    // 查询提现金额(PROCESSING + WITHDRAWN 状态)
+    // 根据 teamId 查询提现金额
+    const withdrawAmountStats = teamIds.length > 0
+      ? await this.financeRepository
+          .createQueryBuilder('finance')
+          .select(['finance.teamId as teamId', 'SUM(finance.reminderAmount) as withdrawAmount'])
+          .where('finance.delFlag = :delFlag', { delFlag: false })
+          .andWhere('finance.status IN (:...statuses)', { statuses: [FinanceStatus.PROCESSING, FinanceStatus.WITHDRAWN] })
+          .andWhere('finance.teamId IN (:...teamIds)', { teamIds })
+          .groupBy('finance.teamId')
+          .getRawMany()
+      : []
+
+    const withdrawAmountMap = new Map<number, number>()
+    withdrawAmountStats.forEach(stat => {
+      withdrawAmountMap.set(stat.teamId, Number(stat.withdrawAmount) || 0)
+    })
+
     // 计算统计数据
     const statistics = {
       totalTeams: teams.length,
@@ -318,12 +350,14 @@ export class TeamService {
       todayDAU: 0,
       todayNewUsers: 0,
       averageCommissionRate: 0,
+      balance: 0,
       allTeams: [] as Array<{ id: number; name: string; totalRevenue: number; todayRevenue: number; totalSales: number; todaySales: number; todayDAU: number; todayNewUsers: number; totalUsers: number }>
     }
 
     let totalCommissionRate = 0
 
     // 构建团队列表并计算总计(使用 team.userId 进行映射)
+    let totalWithdrawAmount = 0
     teams.forEach(team => {
       const teamTotalRevenue = totalRevenueMap.get(team.userId) || 0
       const teamTodayRevenue = todayRevenueMap.get(team.userId) || 0
@@ -332,6 +366,7 @@ export class TeamService {
       const teamTodayDAU = todayDAUMap.get(team.id) || 0
       const teamTodayNewUsers = todayNewUsersMap.get(team.id) || 0
       const teamTotalUsers = totalUsersMap.get(team.id) || 0
+      const teamWithdrawAmount = withdrawAmountMap.get(team.id) || 0
 
       statistics.totalRevenue += teamTotalRevenue
       statistics.todayRevenue += teamTodayRevenue
@@ -339,6 +374,7 @@ export class TeamService {
       statistics.todaySales += teamTodaySales
       statistics.todayDAU += teamTodayDAU
       statistics.todayNewUsers += teamTodayNewUsers
+      totalWithdrawAmount += teamWithdrawAmount
       totalCommissionRate += Number(team.commissionRate)
 
       statistics.allTeams.push({
@@ -387,6 +423,9 @@ export class TeamService {
 
     statistics.averageCommissionRate = teams.length > 0 ? Number((totalCommissionRate / teams.length).toFixed(2)) : 0
 
+    // 计算余额:所有收益 - 提现金额(提现中 + 已提现)
+    statistics.balance = Number((statistics.totalRevenue - totalWithdrawAmount).toFixed(5))
+
     // 按总收入排序,但确保默认团队始终在最后
     statistics.allTeams.sort((a, b) => {
       // 如果其中一个是默认团队(id 为 0),则默认团队排在后面
@@ -672,4 +711,137 @@ export class TeamService {
       totalUsers
     }
   }
+
+  /**
+   * 生成一级代理链接(不带code,直接域名跳转)
+   * 一级代理就是团队(team),效仿普通代理链接,生成 generalLink 和 browserLink 两种类型
+   * @param teamId 团队ID
+   * @returns 一级代理链接(generalLink 和 browserLink)
+   */
+  async generateFirstLevelAgentLink(teamId: number): Promise<{ generalLink: string; browserLink: string }> {
+    const team = await this.findById(teamId)
+
+    let domain = ''
+
+    // 第一步:尝试获取当前团队的一级域名
+    try {
+      const primaryDomain = await this.domainManagementRepository.findOne({
+        where: {
+          teamId: team.id,
+          domainType: DomainManagementType.PRIMARY,
+          enabled: true
+        },
+        order: { createdAt: 'DESC' }
+      })
+
+      if (primaryDomain && primaryDomain.domain) {
+        domain = primaryDomain.domain.trim()
+        this.app.log.info(`使用团队一级域名: ${domain}`)
+      }
+    } catch (error) {
+      this.app.log.warn({ err: error }, '查询团队一级域名失败,将使用公用配置')
+    }
+
+    // 第二步:如果团队没有一级域名,使用公用的 super_domain 配置
+    if (!domain || domain.trim() === '') {
+      try {
+        const config = await this.sysConfigService.getSysConfig('super_domain')
+        domain = config.value
+        this.app.log.info(`使用公用配置 super_domain: ${domain}`)
+      } catch (error) {
+        this.app.log.warn({ err: error }, '未找到 super_domain 配置')
+        domain = ''
+      }
+    }
+
+    // 如果都没有配置,抛出错误
+    if (!domain || domain.trim() === '') {
+      throw new Error('系统未配置域名(团队一级域名或 super_domain),无法生成一级代理链接')
+    }
+
+    // 确保域名以 http:// 或 https:// 开头
+    let finalDomain = domain.trim()
+    if (!finalDomain.startsWith('http://') && !finalDomain.startsWith('https://')) {
+      finalDomain = `https://${finalDomain}`
+    }
+
+    // 确保域名后面有 /,如果没有则添加
+    if (!finalDomain.endsWith('/')) {
+      finalDomain = `${finalDomain}/`
+    }
+
+    // 生成随机字符串作为假参数 t 的值
+    const randomToken = randomstring.generate({
+      length: 16,
+      charset: 'alphanumeric'
+    })
+
+    // 生成一级代理链接:直接域名,添加 t= 随机字符串参数
+    const generalLink = `${finalDomain}?t=${randomToken}`
+    // 浏览器链接增加 redirect=1 参数和 t= 随机字符串参数
+    const browserLink = `${finalDomain}?t=${randomToken}&redirect=1`
+    
+    // 创建或更新 PromotionLink 记录
+    // 注意:团队的一级代理链接,memberId 为 null
+    try {
+      await this.promotionLinkService.createOrUpdateByMemberIdAndType({
+        teamId: team.id,
+        memberId: null, // 团队的一级代理链接,memberId 为 null
+        name: `${team.name}的微信直开`,
+        image: '',
+        link: generalLink,
+        type: LinkType.GENERAL
+      })
+      await this.promotionLinkService.createOrUpdateByMemberIdAndType({
+        teamId: team.id,
+        memberId: null, // 团队的一级代理链接,memberId 为 null
+        name: `${team.name}的微信跳转(稳定)`,
+        image: '',
+        link: browserLink,
+        type: LinkType.BROWSER
+      })
+    } catch (error) {
+      // 如果创建或更新记录失败,记录日志但不影响返回链接
+      this.app.log.warn({ err: error }, '创建或更新一级代理链接记录失败')
+    }
+    
+    return { generalLink, browserLink }
+  }
+
+  /**
+   * 修改团队提现密码
+   * @param teamId 团队ID
+   * @param oldPassword 旧密码(如果已设置过密码,需要提供旧密码)
+   * @param newPassword 新密码
+   */
+  async updateWithdrawPassword(teamId: number, oldPassword: string | undefined, newPassword: string): Promise<void> {
+    const team = await this.findById(teamId)
+    
+    // 如果已设置过提现密码,需要验证旧密码
+    if (team.withdrawPassword) {
+      if (!oldPassword) {
+        throw new Error('请提供旧密码')
+      }
+      const isPasswordValid = await bcrypt.compare(oldPassword, team.withdrawPassword)
+      if (!isPasswordValid) {
+        throw new Error('旧密码错误')
+      }
+    }
+    
+    // 加密新密码
+    const hashedPassword = await bcrypt.hash(newPassword, 10)
+    
+    // 更新提现密码
+    await this.teamRepository.update(teamId, { withdrawPassword: hashedPassword })
+  }
+
+  /**
+   * 检查团队是否已设置提现密码
+   * @param teamId 团队ID
+   * @returns 是否已设置提现密码
+   */
+  async checkWithdrawPasswordStatus(teamId: number): Promise<boolean> {
+    const team = await this.findById(teamId)
+    return !!team.withdrawPassword
+  }
 }

+ 117 - 0
src/services/video-feedback.service.ts

@@ -0,0 +1,117 @@
+import { FastifyInstance } from 'fastify'
+import Redis from 'ioredis'
+import { VideoFeedback } from '../dto/video-feedback.dto'
+import { PaginationResponse } from '../dto/common.dto'
+
+export class VideoFeedbackService {
+  private redis: Redis | null
+
+  constructor(app: FastifyInstance) {
+    this.redis = app.redis || null
+  }
+
+  /**
+   * 提交视频反馈
+   * @param videoId 视频ID
+   * @param userId 用户ID
+   * @param reason 反馈理由
+   * @param url 当前访问链接(可选)
+   * @returns 是否成功
+   */
+  async submitFeedback(videoId: number, userId: number, reason: string, url?: string): Promise<boolean> {
+    if (!this.redis) {
+      throw new Error('Redis未配置')
+    }
+
+    if (!videoId || !userId || !reason || reason.trim() === '') {
+      throw new Error('参数不完整')
+    }
+
+    // Redis Key格式: video:feedback:{videoId}:{userId}
+    const redisKey = `video:feedback:${videoId}:${userId}`
+
+    // 构建反馈数据
+    const feedback: VideoFeedback = {
+      videoId,
+      userId,
+      reason: reason.trim(),
+      timestamp: Math.floor(Date.now() / 1000),
+      url: url || undefined
+    }
+
+    // 存储到Redis,使用JSON格式
+    await this.redis.set(redisKey, JSON.stringify(feedback))
+    
+    // 设置过期时间为30天(一个月)
+    await this.redis.expire(redisKey, 30 * 24 * 3600)
+
+    return true
+  }
+
+  /**
+   * 获取反馈列表(分页)
+   * @param page 页码(从0开始)
+   * @param size 每页数量
+   * @returns 分页响应
+   */
+  async getFeedbackList(page: number = 0, size: number = 20): Promise<PaginationResponse<VideoFeedback>> {
+    if (!this.redis) {
+      return {
+        content: [],
+        metadata: {
+          total: 0,
+          page: page || 0,
+          size: size || 20
+        }
+      }
+    }
+
+    // 获取所有反馈的key
+    const pattern = 'video:feedback:*'
+    const keys = await this.redis.keys(pattern)
+
+    if (keys.length === 0) {
+      return {
+        content: [],
+        metadata: {
+          total: 0,
+          page: page || 0,
+          size: size || 20
+        }
+      }
+    }
+
+    // 获取所有反馈数据
+    const feedbacks: VideoFeedback[] = []
+    for (const key of keys) {
+      const data = await this.redis.get(key)
+      if (data) {
+        try {
+          feedbacks.push(JSON.parse(data) as VideoFeedback)
+        } catch (error) {
+          // 忽略解析错误的数据
+        }
+      }
+    }
+
+    // 按时间戳倒序排序
+    feedbacks.sort((a, b) => b.timestamp - a.timestamp)
+
+    // 分页处理
+    const pageNum = page || 0
+    const sizeNum = size || 20
+    const start = pageNum * sizeNum
+    const end = start + sizeNum
+    const paginatedFeedbacks = feedbacks.slice(start, end)
+
+    return {
+      content: paginatedFeedbacks,
+      metadata: {
+        total: feedbacks.length,
+        page: pageNum,
+        size: sizeNum
+      }
+    }
+  }
+}
+

+ 2 - 8
src/types/fastify.d.ts

@@ -47,17 +47,11 @@ declare module 'fastify' {
     bannerStatisticsScheduler?: BannerStatisticsScheduler
   }
 
-  interface FastifyRequest {
-    file(): Promise<{
+  interface RouteGenericInterface {
+    file?(): Promise<{
       filename: string
       mimetype: string
       toBuffer(): Promise<Buffer>
     } | null>
-    user?: {
-      id: number
-      name: string
-      role: string
-      iat?: number
-    }
   }
 }

+ 242 - 0
test-video-real-url.ts

@@ -0,0 +1,242 @@
+/**
+ * 测试获取视频真实播放地址功能
+ * 使用方法: ts-node test-video-real-url.ts
+ * 或者: npx ts-node test-video-real-url.ts
+ */
+
+import axios from 'axios'
+import dotenv from 'dotenv'
+
+// 加载环境变量
+dotenv.config()
+
+// 配置
+const BASE_URL = process.env.BASE_URL || 'http://localhost:3010'
+const TEST_VIDEO_ID = 38230
+
+// 颜色输出工具
+const colors = {
+  reset: '\x1b[0m',
+  bright: '\x1b[1m',
+  green: '\x1b[32m',
+  red: '\x1b[31m',
+  yellow: '\x1b[33m',
+  blue: '\x1b[34m',
+  cyan: '\x1b[36m'
+}
+
+function log(message: string, color: string = colors.reset) {
+  console.log(`${color}${message}${colors.reset}`)
+}
+
+function logSuccess(message: string) {
+  log(`✅ ${message}`, colors.green)
+}
+
+function logError(message: string) {
+  log(`❌ ${message}`, colors.red)
+}
+
+function logInfo(message: string) {
+  log(`ℹ️  ${message}`, colors.blue)
+}
+
+function logWarning(message: string) {
+  log(`⚠️  ${message}`, colors.yellow)
+}
+
+/**
+ * 测试获取视频真实播放地址
+ */
+async function testGetVideoRealUrl(videoId: number) {
+  try {
+    logInfo(`\n开始测试获取视频真实播放地址 (视频ID: ${videoId})...`)
+    
+    const url = `${BASE_URL}/api/video/real-url?id=${videoId}`
+    log(`请求URL: ${url}`, colors.cyan)
+    
+    const response = await axios.get(url, {
+      timeout: 10000,
+      validateStatus: () => true // 接受所有状态码
+    })
+    
+    log(`\n响应状态码: ${response.status}`, colors.cyan)
+    log(`响应数据:`, colors.cyan)
+    console.log(JSON.stringify(response.data, null, 2))
+    
+    if (response.status === 200 && response.data.code === 1) {
+      logSuccess('获取视频播放地址成功!')
+      log(`\n视频ID: ${response.data.data.id}`)
+      if (response.data.data.originalUrl) {
+        log(`\n原始获取地址:`, colors.green)
+        log(`${response.data.data.originalUrl}`, colors.bright)
+      }
+      if (response.data.data.proxyUrl) {
+        log(`\n代理地址:`, colors.yellow)
+        log(`${response.data.data.proxyUrl}`, colors.bright)
+      }
+      if (response.data.data.realUrl) {
+        log(`\n真实地址(替换过域名):`, colors.cyan)
+        log(`${response.data.data.realUrl}`, colors.bright)
+      }
+      if (response.data.data.bestUrl) {
+        log(`\n最佳播放地址:`, colors.bright + colors.green)
+        log(`${response.data.data.bestUrl}`, colors.bright)
+      }
+      return response.data.data
+    } else {
+      logError(`获取失败: ${response.data.msg || '未知错误'}`)
+      return null
+    }
+  } catch (error: any) {
+    if (axios.isAxiosError(error)) {
+      if (error.response) {
+        logError(`请求失败: ${error.response.status} ${error.response.statusText}`)
+        logError(`错误信息: ${JSON.stringify(error.response.data, null, 2)}`)
+      } else if (error.request) {
+        logError('请求已发送但没有收到响应')
+        logError(`请确保服务器正在运行: ${BASE_URL}`)
+      } else {
+        logError(`请求配置错误: ${error.message}`)
+      }
+    } else {
+      logError(`未知错误: ${error.message || String(error)}`)
+    }
+    return null
+  }
+}
+
+/**
+ * 测试获取视频详情(对比用)
+ */
+async function testGetVideoDetail(videoId: number) {
+  try {
+    logInfo(`\n开始测试获取视频详情 (视频ID: ${videoId})...`)
+    
+    const url = `${BASE_URL}/api/video/detail?id=${videoId}`
+    log(`请求URL: ${url}`, colors.cyan)
+    
+    const response = await axios.get(url, {
+      timeout: 10000,
+      validateStatus: () => true
+    })
+    
+    log(`\n响应状态码: ${response.status}`, colors.cyan)
+    
+    if (response.status === 200 && response.data.code === 1) {
+      logSuccess('获取视频详情成功!')
+      log(`\n视频ID: ${response.data.data.id}`)
+      log(`视频标题: ${response.data.data.title}`)
+      log(`播放地址(代理后): ${response.data.data.m3u8}`, colors.yellow)
+      return response.data.data.m3u8
+    } else {
+      logError(`获取失败: ${response.data.msg || '未知错误'}`)
+      return null
+    }
+  } catch (error: any) {
+    if (axios.isAxiosError(error)) {
+      if (error.response) {
+        logError(`请求失败: ${error.response.status} ${error.response.statusText}`)
+      } else if (error.request) {
+        logError('请求已发送但没有收到响应')
+      } else {
+        logError(`请求配置错误: ${error.message}`)
+      }
+    } else {
+      logError(`未知错误: ${error.message || String(error)}`)
+    }
+    return null
+  }
+}
+
+/**
+ * 对比三个地址
+ */
+function compareUrls(urlData: { originalUrl?: string | null; proxyUrl?: string | null; realUrl?: string | null; bestUrl?: string | null } | null) {
+  if (!urlData) {
+    return
+  }
+  
+  log(`\n${'='.repeat(60)}`, colors.cyan)
+  log('地址对比:', colors.bright)
+  log(`${'='.repeat(60)}`, colors.cyan)
+  
+  if (urlData.originalUrl) {
+    log(`\n原始获取地址:`, colors.green)
+    log(`${urlData.originalUrl}`, colors.bright)
+  }
+  
+  if (urlData.proxyUrl) {
+    log(`\n代理播放地址:`, colors.yellow)
+    log(`${urlData.proxyUrl}`, colors.bright)
+  }
+  
+  if (urlData.realUrl) {
+    log(`\n真实播放地址(替换过域名):`, colors.cyan)
+    log(`${urlData.realUrl}`, colors.bright)
+  }
+  
+  if (urlData.bestUrl) {
+    log(`\n最佳播放地址:`, colors.bright + colors.green)
+    log(`${urlData.bestUrl}`, colors.bright)
+  }
+  
+  // 检查地址是否不同
+  if (urlData.originalUrl && urlData.realUrl && urlData.originalUrl !== urlData.realUrl) {
+    logSuccess('\n✓ 真实地址已替换域名(与原始地址不同)')
+  } else if (urlData.originalUrl && urlData.realUrl) {
+    logWarning('\n⚠ 真实地址未替换域名(与原始地址相同,可能未配置 video_source)')
+  }
+  
+  if (urlData.originalUrl && urlData.proxyUrl && urlData.originalUrl !== urlData.proxyUrl) {
+    logSuccess('✓ 代理地址已转换(与原始地址不同)')
+  } else if (urlData.originalUrl && urlData.proxyUrl) {
+    logWarning('⚠ 代理地址未转换(与原始地址相同)')
+  }
+  
+  if (urlData.bestUrl) {
+    try {
+      const bestUrlObj = new URL(urlData.bestUrl)
+      if (bestUrlObj.protocol === 'https:') {
+        logSuccess('✓ 最佳播放地址使用HTTPS,无需代理,性能最佳')
+      } else if (bestUrlObj.protocol === 'http:') {
+        logWarning('⚠ 最佳播放地址使用HTTP代理,可能影响性能')
+      }
+    } catch (e) {
+      // URL解析失败,忽略
+    }
+  }
+}
+
+/**
+ * 主测试函数
+ */
+async function main() {
+  log(`\n${'='.repeat(60)}`, colors.bright)
+  log('视频真实播放地址测试', colors.bright)
+  log(`${'='.repeat(60)}`, colors.bright)
+  log(`\n测试配置:`, colors.cyan)
+  log(`- 服务器地址: ${BASE_URL}`)
+  log(`- 测试视频ID: ${TEST_VIDEO_ID}`)
+  
+  // 测试1: 获取完整地址信息(原始地址、代理地址、真实地址)
+  const urlData = await testGetVideoRealUrl(TEST_VIDEO_ID)
+  
+  // 测试2: 获取视频详情(包含代理地址)
+  const proxyUrl = await testGetVideoDetail(TEST_VIDEO_ID)
+  
+  // 对比地址
+  compareUrls(urlData)
+  
+  log(`\n${'='.repeat(60)}`, colors.bright)
+  log('测试完成', colors.bright)
+  log(`${'='.repeat(60)}`, colors.bright)
+}
+
+// 运行测试
+main().catch(error => {
+  logError(`\n测试执行失败: ${error.message}`)
+  console.error(error)
+  process.exit(1)
+})
+