| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672 |
- 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<Member>
- private dataSource: DataSource
- private vipDurations: Record<VipLevel, number | null> = {
- [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<User> {
- 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<void> {
- 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<Member> = {}
- 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<Member> {
- 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<Member> {
- return this.memberRepository.findOneOrFail({ where: { id } })
- }
- async findByUserId(userId: number): Promise<Member | null> {
- return this.memberRepository.findOne({ where: { userId } })
- }
- async findByEmail(email: string): Promise<Member | null> {
- return this.memberRepository.findOne({ where: { email } })
- }
- async findByPhone(phone: string): Promise<Member | null> {
- return this.memberRepository.findOne({ where: { phone } })
- }
- async list(
- page: number,
- size: number,
- filters?: {
- vipLevel?: VipLevel | VipLevel[]
- status?: MemberStatus | MemberStatus[]
- userId?: number
- }
- ): Promise<PaginationResponse<Member>> {
- 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<Member>): Promise<Member> {
- await this.memberRepository.update(id, data)
- return this.findById(id)
- }
- async delete(id: number): Promise<void> {
- await this.memberRepository.delete(id)
- }
- async updateLastLogin(id: number): Promise<void> {
- await this.memberRepository.update(id, { lastLoginAt: new Date() })
- }
- async updateVipLevel(id: number, vipLevel: VipLevel, vipExpireTime?: Date): Promise<Member> {
- const updateData: Partial<Member> = { 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<Member> {
- return this.update(id, { status })
- }
- async findAllMembers(): Promise<Partial<Member>[]> {
- return this.memberRepository.find({
- select: ['id', 'userId', 'email', 'phone', 'vipLevel', 'status', 'vipExpireTime', 'lastLoginAt']
- })
- }
- async countByVipLevel(): Promise<Record<VipLevel, number>> {
- const result: Record<VipLevel, number> = {} as Record<VipLevel, number>
- for (const level of Object.values(VipLevel)) {
- const count = await this.memberRepository.count({ where: { vipLevel: level } })
- result[level] = count
- }
- return result
- }
- async countByStatus(): Promise<Record<MemberStatus, number>> {
- const result: Record<MemberStatus, number> = {} as Record<MemberStatus, number>
- 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<void> {
- 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<void> {
- 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<Member> {
- 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<boolean> {
- // 查找所有被封禁的member(status为INACTIVE)
- const bannedMembers = await this.memberRepository.find({
- where: { status: MemberStatus.INACTIVE }
- })
-
- // 检查当前IP是否匹配任何被封禁member的ip
- return bannedMembers.some((member: Member) => member.ip === ip)
- }
- }
|