import { In, Repository, DataSource } from 'typeorm' import { FastifyInstance } from 'fastify' import { Member, VipLevel, MemberStatus } from '../entities/member.entity' import { PaginationResponse } from '../dto/common.dto' import { User, UserRole } from '../entities/user.entity' import * as randomstring from 'randomstring' import { Team } from '../entities/team.entity' import { TeamDomain } from '../entities/team-domain.entity' import { TeamMembers } from '../entities/team-members.entity' import { LandingDomainPool } from '../entities/landing-domain-pool.entity' import bcrypt from 'bcryptjs' export class MemberService { private memberRepository: Repository private dataSource: DataSource private vipDurations: Record = { [VipLevel.GUEST]: null, [VipLevel.FREE]: null, [VipLevel.HOURLY]: 60 * 60 * 1000, [VipLevel.DAILY]: 24 * 60 * 60 * 1000, [VipLevel.WEEKLY]: 7 * 24 * 60 * 60 * 1000, [VipLevel.MONTHLY]: 30 * 24 * 60 * 60 * 1000, [VipLevel.QUARTERLY]: 90 * 24 * 60 * 60 * 1000, [VipLevel.YEARLY]: 365 * 24 * 60 * 60 * 1000, [VipLevel.LIFETIME]: new Date('2099-12-31').getTime() - Date.now() } constructor(app: FastifyInstance) { this.memberRepository = app.dataSource.getRepository(Member) this.dataSource = app.dataSource } async createGuest(code?: string, domain?: string, ip?: string, landingDomain?: string): Promise { return await this.dataSource.transaction(async manager => { const randomSuffix = randomstring.generate({ length: 10, charset: 'alphanumeric' }) const guestName = `m_${randomSuffix}` let finalGuestName = guestName let counter = 0 while (await manager.findOne(User, { where: { name: finalGuestName } })) { counter++ finalGuestName = `${guestName}${counter}` } let parentId = 1 let teamId = 0 let domainId = 0 // 校验顺序:推广码 -> 跳转域名 -> 落地域名 // 1. 优先使用推广码 if (code && code.trim() !== '') { // 先尝试查找团队成员的推广码 const teamMember = await manager.findOne(TeamMembers, { where: { promoCode: code } }) if (teamMember) { // 如果找到团队成员,使用团队成员的 userId 作为 parentId parentId = teamMember.userId teamId = teamMember.teamId } else { // 如果没有找到团队成员,尝试查找团队的推广码 const team = await manager.findOne(Team, { where: { affCode: code } }) if (team) { parentId = team.userId teamId = team.id } } } // 2. 如果推广码没有找到,使用跳转域名(team-domain) if (parentId === 1 && teamId === 0 && domain) { let domainName = domain try { if (domain.includes('://')) { const url = new URL(domain) domainName = url.hostname } else { domainName = domain } } catch (error) { domainName = domain } const teamDomain = await manager.findOne(TeamDomain, { where: { domain: domainName } }) if (teamDomain) { // 如果domain绑定到teamMember,使用teamMember的userId作为parentId // 这样在分润时可以从teamMember向上查找到team if (teamDomain.teamMemberId) { const teamMember = await manager.findOne(TeamMembers, { where: { id: teamDomain.teamMemberId } }) if (teamMember) { parentId = teamMember.userId teamId = teamMember.teamId } } else { // 如果domain只绑定到team,使用team的userId作为parentId const team = await manager.findOne(Team, { where: { id: teamDomain.teamId } }) if (team) { parentId = team.userId teamId = team.id } } domainId = teamDomain.id } } // 3. 如果跳转域名没有找到,使用落地域名(landing-domain) if (parentId === 1 && teamId === 0 && landingDomain && landingDomain.trim() !== '') { const landingDomainPoolRepository = manager.getRepository(LandingDomainPool) const landingDomainRecord = await landingDomainPoolRepository.findOne({ where: { domain: landingDomain.trim() } }) if (landingDomainRecord) { // 如果落地域名绑定到团队成员(userId不为空),使用teamMember的userId作为parentId // 这样在分润时可以从teamMember向上查找到team if (landingDomainRecord.userId) { const teamMember = await manager.findOne(TeamMembers, { where: { userId: landingDomainRecord.userId } }) if (teamMember) { parentId = teamMember.userId teamId = teamMember.teamId } } else { // 如果落地域名只绑定到团队,使用team的userId作为parentId teamId = landingDomainRecord.teamId const foundTeam = await manager.findOne(Team, { where: { id: teamId } }) if (foundTeam) { parentId = foundTeam.userId } } } } // 4. 如果以上都没有找到有效注册信息,统一分给当天注册最多的渠道(排除team0和team1) if (parentId === 1 && teamId === 0) { // 查询当天注册最多的渠道(teamId) const today = new Date() today.setHours(0, 0, 0, 0) const tomorrow = new Date(today) tomorrow.setDate(tomorrow.getDate() + 1) // 统计当天每个渠道(teamId)的注册数量,排除teamId为0或1的渠道 const teamCounts = await manager .createQueryBuilder(Member, 'member') .select('member.teamId', 'teamId') .addSelect('COUNT(member.id)', 'count') .where('member.createdAt >= :today', { today }) .andWhere('member.createdAt < :tomorrow', { tomorrow }) .andWhere('member.teamId > 1') // 排除team0和team1 .groupBy('member.teamId') .orderBy('count', 'DESC') .limit(1) .getRawMany() if (teamCounts.length > 0 && teamCounts[0].teamId) { const mostRegisteredTeamId = teamCounts[0].teamId // 根据teamId查找团队,获取userId作为parentId const foundTeam = await manager.findOne(Team, { where: { id: mostRegisteredTeamId } }) if (foundTeam) { parentId = foundTeam.userId teamId = foundTeam.id domainId = 0 } } } const defaultPassword = 'password123' const hashedPassword = await bcrypt.hash(defaultPassword, 10) const user = manager.create(User, { name: finalGuestName, role: UserRole.USER, parentId, password: hashedPassword }) const savedUser = await manager.save(user) const member = manager.create(Member, { userId: savedUser.id, teamId, domainId, vipLevel: VipLevel.GUEST, status: MemberStatus.ACTIVE, ip: ip || 'unknown', lastLoginAt: new Date() }) await manager.save(member) return savedUser }) } async upgradeGuest(userId: number, name: string, password: string, email?: string, phone?: string): Promise { await this.dataSource.transaction(async manager => { const user = await manager.findOne(User, { where: { id: userId } }) if (!user) { throw new Error('用户不存在') } const existingUser = await manager.findOne(User, { where: { name } }) if (existingUser && existingUser.id !== userId) { throw new Error('用户名已被使用') } const hashedPassword = await bcrypt.hash(password, 10) await manager.update(User, userId, { name, password: hashedPassword }) const member = await manager.findOne(Member, { where: { userId } }) if (!member) { throw new Error('会员信息不存在') } if (email) { const existingEmail = await manager.findOne(Member, { where: { email } }) if (existingEmail && existingEmail.id !== member.id) { throw new Error('邮箱已被使用') } } if (phone) { const existingPhone = await manager.findOne(Member, { where: { phone } }) if (existingPhone && existingPhone.id !== member.id) { throw new Error('手机号已被使用') } } const updateData: Partial = {} if (email) { updateData.email = email } if (phone) { updateData.phone = phone } updateData.vipLevel = VipLevel.FREE await manager.update(Member, member.id, updateData) }) } async validateMemberLogin(name: string, password: string): Promise<{ user: User; member: Member } | null> { const user = await this.dataSource.getRepository(User).findOne({ where: { name } }) if (!user) { return null } if (!user.password) { return null } const isPasswordValid = await bcrypt.compare(password, user.password) if (!isPasswordValid) { return null } const member = await this.findByUserId(user.id) if (!member) { return null } if (member.status !== MemberStatus.ACTIVE) { return null } await this.updateLastLogin(member.id) return { user, member } } async create(data: { userId: number teamId?: number domainId?: number email?: string phone?: string vipLevel?: VipLevel status?: MemberStatus vipExpireTime?: Date ip?: string }): Promise { const member = this.memberRepository.create({ userId: data.userId, teamId: data.teamId, domainId: data.domainId, email: data.email, phone: data.phone, vipLevel: data.vipLevel || VipLevel.GUEST, status: data.status || MemberStatus.ACTIVE, vipExpireTime: data.vipExpireTime, ip: data.ip }) return this.memberRepository.save(member) } async findById(id: number): Promise { return this.memberRepository.findOneOrFail({ where: { id } }) } async findByUserId(userId: number): Promise { return this.memberRepository.findOne({ where: { userId } }) } async findByEmail(email: string): Promise { return this.memberRepository.findOne({ where: { email } }) } async findByPhone(phone: string): Promise { return this.memberRepository.findOne({ where: { phone } }) } async list( page: number, size: number, filters?: { vipLevel?: VipLevel | VipLevel[] status?: MemberStatus | MemberStatus[] userId?: number } ): Promise> { const where: any = {} if (filters?.vipLevel) { where.vipLevel = filters.vipLevel instanceof Array ? In(filters.vipLevel) : filters.vipLevel } if (filters?.status) { where.status = filters.status instanceof Array ? In(filters.status) : filters.status } if (filters?.userId) { where.userId = filters.userId } const [members, total] = await this.memberRepository.findAndCount({ skip: (Number(page) || 0) * (Number(size) || 20), take: Number(size) || 20, where, order: { createdAt: 'DESC' } }) return { content: members, metadata: { total: Number(total), page: Number(page), size: Number(size) } } } async update(id: number, data: Partial): Promise { await this.memberRepository.update(id, data) return this.findById(id) } async delete(id: number): Promise { await this.memberRepository.delete(id) } async updateLastLogin(id: number): Promise { await this.memberRepository.update(id, { lastLoginAt: new Date() }) } async updateVipLevel(id: number, vipLevel: VipLevel, vipExpireTime?: Date): Promise { const updateData: Partial = { vipLevel } if (vipExpireTime) { updateData.vipExpireTime = vipExpireTime } else { const duration = this.vipDurations[vipLevel] updateData.vipExpireTime = duration ? new Date(Date.now() + duration) : undefined } return this.update(id, updateData) } async updateStatus(id: number, status: MemberStatus): Promise { return this.update(id, { status }) } async findAllMembers(): Promise[]> { return this.memberRepository.find({ select: ['id', 'userId', 'email', 'phone', 'vipLevel', 'status', 'vipExpireTime', 'lastLoginAt'] }) } async countByVipLevel(): Promise> { const result: Record = {} as Record for (const level of Object.values(VipLevel)) { const count = await this.memberRepository.count({ where: { vipLevel: level } }) result[level] = count } return result } async countByStatus(): Promise> { const result: Record = {} as Record for (const status of Object.values(MemberStatus)) { const count = await this.memberRepository.count({ where: { status } }) result[status] = count } return result } async checkVipExpireTime(member: Member): Promise { if (member.vipExpireTime && member.vipExpireTime < new Date()) { member.vipLevel = VipLevel.FREE await this.update(member.id, member) } } async register( name: string, password: string, email?: string, phone?: string, code?: string, ip?: string, memberCode?: string, landingDomain?: string ): Promise<{ user: User; member: Member }> { return await this.dataSource.transaction(async manager => { // 检查用户名是否已存在 const existingUser = await manager.findOne(User, { where: { name } }) if (existingUser) { throw new Error('用户名已被使用') } // 检查邮箱是否已存在 if (email) { const existingEmail = await manager.findOne(Member, { where: { email } }) if (existingEmail) { throw new Error('邮箱已被使用') } } // 检查手机号是否已存在 if (phone) { const existingPhone = await manager.findOne(Member, { where: { phone } }) if (existingPhone) { throw new Error('手机号已被使用') } } // 获取推荐团队或根据IP查找历史注册信息 // 校验顺序:推广码 -> 落地域名 -> IP历史记录 let team = null let parentId = 1 let teamId = 0 let domainId = 0 // 1. 优先使用推广码(memberCode 或 code) const promoCode = memberCode || code if (promoCode && promoCode.trim() !== '') { // 如果提供了 memberCode,优先查找团队成员的推广码 if (memberCode && memberCode.trim() !== '') { const teamMember = await manager.findOne(TeamMembers, { where: { promoCode: memberCode } }) if (teamMember) { // 如果找到团队成员,使用团队成员的 userId 作为 parentId parentId = teamMember.userId teamId = teamMember.teamId domainId = 0 } } // 如果没有找到团队成员(或者使用的是 code),尝试查找团队的推广码 if (parentId === 1 && teamId === 0) { team = await manager.findOne(Team, { where: { affCode: promoCode } }) if (team) { parentId = team.userId teamId = team.id domainId = 0 } } } // 2. 如果推广码没有找到,使用落地域名(landing-domain) if (parentId === 1 && teamId === 0 && landingDomain && landingDomain.trim() !== '') { const landingDomainPoolRepository = manager.getRepository(LandingDomainPool) const landingDomainRecord = await landingDomainPoolRepository.findOne({ where: { domain: landingDomain.trim() } }) if (landingDomainRecord) { // 如果落地域名绑定到团队成员(userId不为空),使用teamMember的userId作为parentId // 这样在分润时可以从teamMember向上查找到team if (landingDomainRecord.userId) { const teamMember = await manager.findOne(TeamMembers, { where: { userId: landingDomainRecord.userId } }) if (teamMember) { parentId = teamMember.userId teamId = teamMember.teamId } } else { // 如果落地域名只绑定到团队,使用team的userId作为parentId teamId = landingDomainRecord.teamId const foundTeam = await manager.findOne(Team, { where: { id: teamId } }) if (foundTeam) { parentId = foundTeam.userId } } } } // 3. 如果以上都没有找到,根据IP查找历史注册信息 if (parentId === 1 && teamId === 0 && ip && ip !== 'unknown') { // 如果没有推广参数,检查注册IP是否之前注册过账号 const existingMemberByIp = await manager.findOne(Member, { where: { ip }, order: { createdAt: 'DESC' } }) if (existingMemberByIp) { // 如果该IP之前注册过账号,使用之前注册的teamId和domainId teamId = existingMemberByIp.teamId || 0 domainId = existingMemberByIp.domainId || 0 // 如果domainId存在,检查domain是否绑定到teamMember if (domainId > 0) { const teamDomain = await manager.findOne(TeamDomain, { where: { id: domainId } }) if (teamDomain && teamDomain.teamMemberId) { // 如果domain绑定到teamMember,使用teamMember的userId作为parentId const teamMember = await manager.findOne(TeamMembers, { where: { id: teamDomain.teamMemberId } }) if (teamMember) { parentId = teamMember.userId teamId = teamMember.teamId } } else if (teamDomain && teamDomain.teamId) { // 如果domain只绑定到team,使用team的userId作为parentId const existingTeam = await manager.findOne(Team, { where: { id: teamDomain.teamId } }) if (existingTeam) { parentId = existingTeam.userId teamId = existingTeam.id } } } else if (teamId > 0) { // 如果没有domainId,根据teamId查找team,获取userId作为parentId(agentId) const existingTeam = await manager.findOne(Team, { where: { id: teamId } }) if (existingTeam) { parentId = existingTeam.userId } } } } // 4. 如果以上都没有找到有效注册信息,统一分给当天注册最多的渠道(排除team0和team1) if (parentId === 1 && teamId === 0) { // 查询当天注册最多的渠道(teamId) const today = new Date() today.setHours(0, 0, 0, 0) const tomorrow = new Date(today) tomorrow.setDate(tomorrow.getDate() + 1) // 统计当天每个渠道(teamId)的注册数量,排除teamId为0或1的渠道 const teamCounts = await manager .createQueryBuilder(Member, 'member') .select('member.teamId', 'teamId') .addSelect('COUNT(member.id)', 'count') .where('member.createdAt >= :today', { today }) .andWhere('member.createdAt < :tomorrow', { tomorrow }) .andWhere('member.teamId > 1') // 排除team0和team1 .groupBy('member.teamId') .orderBy('count', 'DESC') .limit(1) .getRawMany() if (teamCounts.length > 0 && teamCounts[0].teamId) { const mostRegisteredTeamId = teamCounts[0].teamId // 根据teamId查找团队,获取userId作为parentId const foundTeam = await manager.findOne(Team, { where: { id: mostRegisteredTeamId } }) if (foundTeam) { parentId = foundTeam.userId teamId = foundTeam.id domainId = 0 } } } // 创建用户 const hashedPassword = await bcrypt.hash(password, 10) const user = manager.create(User, { name, password: hashedPassword, role: UserRole.USER, parentId }) const savedUser = await manager.save(user) // 创建会员 const member = manager.create(Member, { userId: savedUser.id, teamId, domainId, email, phone, vipLevel: VipLevel.FREE, status: MemberStatus.ACTIVE, ip: ip || 'unknown', lastLoginAt: new Date() }) const savedMember = await manager.save(member) return { user: savedUser, member: savedMember } }) } async updateProfile(userId: number, name: string, email?: string): Promise { await this.dataSource.transaction(async manager => { // 检查用户名是否已被其他用户使用 const existingUser = await manager.findOne(User, { where: { name } }) if (existingUser && existingUser.id !== userId) { throw new Error('用户名已被使用') } // 检查邮箱是否已被其他会员使用 if (email) { const existingEmail = await manager.findOne(Member, { where: { email } }) if (existingEmail && existingEmail.userId !== userId) { throw new Error('邮箱已被使用') } } // 更新用户名 await manager.update(User, userId, { name }) // 更新会员邮箱 if (email) { const member = await manager.findOne(Member, { where: { userId } }) if (member) { await manager.update(Member, member.id, { email }) } } }) } /** * 封禁当前用户的member * @param userId 用户ID * @returns 封禁后的member */ async banMember(userId: number): Promise { const member = await this.findByUserId(userId) if (!member) { throw new Error('会员信息不存在') } // 将status修改为INACTIVE return await this.updateStatus(member.id, MemberStatus.INACTIVE) } /** * 检查当前IP是否在被封禁的member的ip中 * @param ip 要检查的IP地址 * @returns 如果IP被封禁返回true,否则返回false */ async checkBan(ip: string): Promise { // 查找所有被封禁的member(status为INACTIVE) const bannedMembers = await this.memberRepository.find({ where: { status: MemberStatus.INACTIVE } }) // 检查当前IP是否匹配任何被封禁member的ip return bannedMembers.some((member: Member) => member.ip === ip) } }