Bläddra i källkod

优化推广账号后台展示逻辑,团队成员统计优化

wilhelm wong 2 månader sedan
förälder
incheckning
259d49d97d

+ 35 - 1
src/controllers/team-members.controller.ts

@@ -7,7 +7,9 @@ import {
   TeamMembersParams,
   UpdateRevenueBody,
   TeamMemberStatsQuery,
-  TeamMemberStatsResponse
+  TeamMemberStatsResponse,
+  TeamLeaderStatsQuery,
+  TeamLeaderStatsResponse
 } from '../dto/team-members.dto'
 import { UserRole } from '../entities/user.entity'
 import { TeamService } from '../services/team.service'
@@ -233,4 +235,36 @@ export class TeamMembersController {
       return reply.code(500).send({ message: '获取团队成员统计信息失败' })
     }
   }
+
+  /**
+   * 获取团队统计数据(队长视角)
+   */
+  async getTeamLeaderStats(request: FastifyRequest<{ Querystring: TeamLeaderStatsQuery }>, reply: FastifyReply) {
+    try {
+      const user = request.user
+      if (!user) {
+        return reply.code(403).send({ message: '用户未登录' })
+      }
+
+      // 只有团队用户(队长)可以访问此接口
+      if (user.role !== UserRole.TEAM) {
+        return reply.code(403).send({ message: '只有团队用户才能访问此接口' })
+      }
+
+      // 获取团队信息
+      const team = await this.teamService.findByUserId(user.id)
+      request.query.teamId = team.id
+
+      const stats = await this.teamMembersService.getTeamLeaderStats(request.query, user.id)
+
+      return reply.send(stats)
+    } catch (error) {
+      console.error('获取团队统计数据失败:', error)
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      return reply.code(500).send({ 
+        message: '获取团队统计数据失败', 
+        error: errorMessage
+      })
+    }
+  }
 }

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

@@ -61,3 +61,28 @@ export interface TeamMemberStatsResponse {
     endDate: string
   }
 }
+
+export interface TeamLeaderStatsQuery {
+  teamId?: number
+}
+
+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
+  }>
+}

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

@@ -8,7 +8,8 @@ import {
   ListTeamMembersQuery,
   TeamMembersParams,
   UpdateRevenueBody,
-  TeamMemberStatsQuery
+  TeamMemberStatsQuery,
+  TeamLeaderStatsQuery
 } from '../dto/team-members.dto'
 
 export default async function teamMembersRoutes(fastify: FastifyInstance) {
@@ -76,4 +77,11 @@ export default async function teamMembersRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate, hasAnyRole(UserRole.ADMIN, UserRole.TEAM, UserRole.PROMOTER)] },
     teamMembersController.getTeamMemberStats.bind(teamMembersController)
   )
+
+  // 获取团队统计数据(队长视角)
+  fastify.get<{ Querystring: TeamLeaderStatsQuery }>(
+    '/statistics/team-leader',
+    { onRequest: [authenticate, hasRole(UserRole.TEAM)] },
+    teamMembersController.getTeamLeaderStats.bind(teamMembersController)
+  )
 }

+ 23 - 17
src/services/payment.service.ts

@@ -153,7 +153,7 @@ export class PaymentService {
           }
         }
       } catch (memberError) {
-        this.app.log.warn(`Failed to find team member ${teamDomain.teamMemberId}:`, memberError)
+        this.app.log.warn(`Failed to find team member ${teamDomain.teamMemberId}:`,member, memberError)
       }
 
       // 个人分成配置失败,只给团队分成
@@ -213,7 +213,7 @@ export class PaymentService {
       }
       return { agentId: 0, commissionRate: 0, isPersonalCommission: false }
     } catch (error) {
-      this.app.log.warn('Failed to get team commission:', error)
+      this.app.log.warn('Failed to get team commission:',member, error)
       return { agentId: 0, commissionRate: 0, isPersonalCommission: false }
     }
   }
@@ -240,7 +240,7 @@ export class PaymentService {
       // 如果域名绑定的团队也没有分成比例,回退到member的团队
       return await this.getTeamCommission(member)
     } catch (error) {
-      this.app.log.warn('Failed to get team commission from domain:', error)
+      this.app.log.warn('Failed to get team commission from domain:',member, error)
       return await this.getTeamCommission(member)
     }
   }
@@ -266,7 +266,7 @@ export class PaymentService {
       }
       return { agentId: 0, commissionRate: 0, isPersonalCommission: false }
     } catch (error) {
-      this.app.log.warn('Failed to get fallback commission:', error)
+      this.app.log.warn('Failed to get fallback commission:',user, error)
       return { agentId: 0, commissionRate: 0, isPersonalCommission: false }
     }
   }
@@ -301,7 +301,7 @@ export class PaymentService {
 
       return data
     } catch (error) {
-      this.app.log.error('Failed to create payment order:', error)
+      this.app.log.error({ err: error }, 'Failed to create payment order')
       throw error
     }
   }
@@ -334,10 +334,10 @@ export class PaymentService {
       this.app.log.info('Member level updated successfully')
       this.app.log.info('Payment processing completed')
     } else {
-      this.app.log.warn('Payment failed', {
-        out_trade_no: params.out_trade_no,
-        trade_status: params.trade_status
-      })
+        this.app.log.warn(
+            { out_trade_no: params.out_trade_no, trade_status: params.trade_status },
+            'Payment failed'
+          )
       throw new Error('Payment failed')
     }
   }
@@ -402,7 +402,10 @@ export class PaymentService {
           }
         } catch (memberError) {
           // 获取member信息失败,使用原有逻辑
-          this.app.log.warn(`Failed to get member info for user ${user.id}, using fallback commission logic:`, memberError)
+          this.app.log.warn(
+            { err: memberError, userId: user.id },
+            `Failed to get member info for user ${user.id}, using fallback commission logic`
+          )
           const fallbackCommission = await this.getFallbackCommission(user)
           commissionInfo = {
             teamAgentId: fallbackCommission.agentId,
@@ -451,7 +454,7 @@ export class PaymentService {
           payNo: result.trade_no || ''
         })
       } catch (incomeError) {
-        this.app.log.error('Failed to create income record:', incomeError)
+        this.app.log.error({ err: incomeError }, 'Failed to create income record')
         throw new Error('操作失败')
       }
 
@@ -462,7 +465,7 @@ export class PaymentService {
         out_trade_no: result.out_trade_no
       }
     } catch (error) {
-      this.app.log.error('Failed to purchase member:', error)
+        this.app.log.error({ err: error }, 'Failed to purchase member')
       throw error
     }
   }
@@ -530,7 +533,10 @@ export class PaymentService {
           }
         } catch (memberError) {
           // 获取member信息失败,使用原有逻辑
-          this.app.log.warn(`Failed to get member info for user ${user.id}, using fallback commission logic:`, memberError)
+          this.app.log.warn(
+            { err: memberError, userId: user.id },
+            `Failed to get member info for user ${user.id}, using fallback commission logic`
+          )
           const fallbackCommission = await this.getFallbackCommission(user)
           commissionInfo = {
             teamAgentId: fallbackCommission.agentId,
@@ -580,7 +586,7 @@ export class PaymentService {
           resourceId: params.resourceId.toString()
         })
       } catch (incomeError) {
-        this.app.log.error('Failed to create income record:', incomeError)
+        this.app.log.error({ err: incomeError }, 'Failed to create income record')
         throw new Error('Failed to create income record')
       }
 
@@ -591,7 +597,7 @@ export class PaymentService {
         out_trade_no: result.out_trade_no
       }
     } catch (error) {
-      this.app.log.error('Failed to create single payment order:', error)
+      this.app.log.error({ err: error }, 'Failed to create single payment order')
       throw error
     }
   }
@@ -629,7 +635,7 @@ export class PaymentService {
 
       return data
     } catch (error) {
-      this.app.log.error('Failed to query payment order:', error)
+      this.app.log.error({ err: error }, 'Failed to query payment order')
       throw error
     }
   }
@@ -694,7 +700,7 @@ export class PaymentService {
         }
       }
     } catch (error) {
-      this.app.log.error('Failed to query user order:', error)
+      this.app.log.error({ err: error }, 'Failed to query user order')
       responseData.msg = '查询订单失败'
     }
 

+ 124 - 64
src/services/team-members.service.ts

@@ -6,7 +6,7 @@ import { Member } from '../entities/member.entity'
 import { TeamDomain } from '../entities/team-domain.entity'
 import { Team } from '../entities/team.entity'
 import { PaginationResponse } from '../dto/common.dto'
-import { CreateTeamMembersBody, UpdateTeamMembersBody, ListTeamMembersQuery, TeamMemberStatsQuery, TeamMemberStatsResponse } from '../dto/team-members.dto'
+import { CreateTeamMembersBody, UpdateTeamMembersBody, ListTeamMembersQuery, TeamMemberStatsQuery, TeamMemberStatsResponse, TeamLeaderStatsQuery, TeamLeaderStatsResponse } from '../dto/team-members.dto'
 import { UserService } from './user.service'
 import { UserRole } from '../entities/user.entity'
 
@@ -107,58 +107,8 @@ export class TeamMembersService {
       order: { createdAt: 'DESC' }
     })
 
-    // 为每个成员计算实际收入(从income_records表统计)
-    const membersWithRevenue = await Promise.all(
-      members.map(async (member) => {
-        // 统计总收入 - 只计算个人分成金额
-        const totalRevenueRecords = await this.incomeRecordsRepository
-          .createQueryBuilder('record')
-          .where('record.personalAgentId = :teamMemberUserId', { 
-            teamMemberUserId: member.userId 
-          })
-          .andWhere('record.delFlag = :delFlag', { delFlag: false })
-          .andWhere('record.status = :status', { status: true })
-          .andWhere('record.personalIncomeAmount > 0')
-          .getMany()
-
-        const totalRevenue = totalRevenueRecords.reduce((sum, record) => {
-          // 只统计个人分成金额
-          return sum + Number(record.personalIncomeAmount || 0)
-        }, 0)
-
-        // 统计今日收入 - 只计算个人分成金额
-        const today = new Date()
-        const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate())
-        const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59)
-
-        const todayRevenueRecords = await this.incomeRecordsRepository
-          .createQueryBuilder('record')
-          .where('record.personalAgentId = :teamMemberUserId', { 
-            teamMemberUserId: member.userId 
-          })
-          .andWhere('record.delFlag = :delFlag', { delFlag: false })
-          .andWhere('record.status = :status', { status: true })
-          .andWhere('record.personalIncomeAmount > 0')
-          .andWhere('record.createdAt >= :todayStart', { todayStart })
-          .andWhere('record.createdAt <= :todayEnd', { todayEnd })
-          .getMany()
-
-        const todayRevenue = todayRevenueRecords.reduce((sum, record) => {
-          // 只统计个人分成金额
-          return sum + Number(record.personalIncomeAmount || 0)
-        }, 0)
-
-        // 返回更新后的成员数据
-        return {
-          ...member,
-          totalRevenue: totalRevenue,
-          todayRevenue: todayRevenue
-        }
-      })
-    )
-
     return {
-      content: membersWithRevenue,
+      content: members,
       metadata: {
         total: Number(total),
         page: Number(page) || 0,
@@ -167,6 +117,7 @@ export class TeamMembersService {
     }
   }
 
+
   async update(data: UpdateTeamMembersBody): Promise<TeamMembers> {
     const { id, ...updateData } = data
 
@@ -250,29 +201,29 @@ export class TeamMembersService {
           .getRawMany()
       : []
 
-    // 查询所有成员的总售卖金额统计
+    // 查询所有成员的总售卖金额统计(按个人代理归属)
     const totalSalesStats = memberUserIds.length > 0
       ? await this.incomeRecordsRepository
           .createQueryBuilder('record')
-          .select(['record.userId as userId', 'SUM(record.orderPrice) as totalSales'])
+          .select(['record.personalAgentId as personalAgentId', 'SUM(record.orderPrice) as totalSales'])
           .where('record.delFlag = :delFlag', { delFlag: false })
           .andWhere('record.status = :status', { status: true })
-          .andWhere('record.userId IN (:...memberUserIds)', { memberUserIds })
-          .groupBy('record.userId')
+          .andWhere('record.personalAgentId IN (:...memberUserIds)', { memberUserIds })
+          .groupBy('record.personalAgentId')
           .getRawMany()
       : []
 
-    // 查询所有成员的今日售卖金额统计
+    // 查询所有成员的今日售卖金额统计(按个人代理归属)
     const todaySalesStats = memberUserIds.length > 0
       ? await this.incomeRecordsRepository
           .createQueryBuilder('record')
-          .select(['record.userId as userId', 'SUM(record.orderPrice) as todaySales'])
+          .select(['record.personalAgentId as personalAgentId', 'SUM(record.orderPrice) as todaySales'])
           .where('record.delFlag = :delFlag', { delFlag: false })
           .andWhere('record.status = :status', { status: true })
           .andWhere('record.createdAt >= :today', { today })
           .andWhere('record.createdAt <= :todayEnd', { todayEnd })
-          .andWhere('record.userId IN (:...memberUserIds)', { memberUserIds })
-          .groupBy('record.userId')
+          .andWhere('record.personalAgentId IN (:...memberUserIds)', { memberUserIds })
+          .groupBy('record.personalAgentId')
           .getRawMany()
       : []
 
@@ -316,12 +267,12 @@ export class TeamMembersService {
       todayRevenueMap.set(stat.userId, Number(stat.todayRevenue) || 0)
     })
 
-    totalSalesStats.forEach(stat => {
-      totalSalesMap.set(stat.userId, Number(stat.totalSales) || 0)
+    totalSalesStats.forEach((stat: any) => {
+      totalSalesMap.set(stat.personalAgentId, Number(stat.totalSales) || 0)
     })
 
-    todaySalesStats.forEach(stat => {
-      todaySalesMap.set(stat.userId, Number(stat.todaySales) || 0)
+    todaySalesStats.forEach((stat: any) => {
+      todaySalesMap.set(stat.personalAgentId, Number(stat.todaySales) || 0)
     })
 
     todayDAUStats.forEach((stat: any) => {
@@ -556,4 +507,113 @@ export class TeamMembersService {
 
     return domainStats
   }
+
+  /**
+   * 获取团队统计数据(队长视角)
+   */
+  async getTeamLeaderStats(query: TeamLeaderStatsQuery, teamLeaderUserId: number): Promise<TeamLeaderStatsResponse> {
+    const { teamId } = query
+
+    // 获取团队信息
+    const team = await this.teamRepository.findOne({ where: { id: teamId } })
+    if (!team) {
+      throw new Error('团队不存在')
+    }
+
+    // 获取团队成员列表
+    const members = await this.teamMembersRepository.find({
+      where: { teamId: teamId },
+      order: { createdAt: 'DESC' }
+    })
+
+    // 计算今日时间范围
+    const today = new Date()
+    const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate())
+    const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59)
+
+    // 一次性查询团队成员的全部收入记录
+    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 membersStats = await Promise.all(
+      members.map(async (member) => {
+        const memberRecords = recordsByAgent.get(member.userId) || []
+
+        const totalRevenue = memberRecords
+          .filter(r => Number(r.personalIncomeAmount || 0) > 0)
+          .reduce((sum, r) => sum + Number(r.personalIncomeAmount || 0), 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)
+
+        const totalSales = memberRecords.reduce((sum, r) => sum + Number(r.orderPrice || 0), 0)
+
+        const todaySales = memberRecords
+          .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
+
+        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))
+        }
+      })
+    )
+
+    return {
+      teamId: team.id,
+      teamName: team.name,
+      teamCommissionRate: Number(Number(team.commissionRate || 0).toFixed(2)),
+      membersStats
+    }
+  }
 }