|
|
@@ -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
|
|
|
+ }
|
|
|
}
|