Răsfoiți Sursa

更新成员控制器,新增游客升级为用户、会员登录、获取会员信息和重置密码的逻辑,同时在服务中实现相关功能,调整路由以支持新接口。

wuyi 3 luni în urmă
părinte
comite
a4a6d376da

+ 138 - 34
src/controllers/member.controller.ts

@@ -1,13 +1,24 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
 import { MemberService } from '../services/member.service'
-import { CreateMemberBody, UpdateMemberBody, ListMemberQuery, MemberResponse } from '../dto/member.dto'
+import {
+  CreateMemberBody,
+  UpdateMemberBody,
+  ListMemberQuery,
+  MemberResponse,
+  UpdateGuestBody,
+  MemberLoginBody,
+  ResetPasswordBody
+} from '../dto/member.dto'
 import { VipLevel, MemberStatus } from '../entities/member.entity'
+import { UserService } from '../services/user.service'
 
 export class MemberController {
   private memberService: MemberService
+  private userService: UserService
 
   constructor(app: FastifyInstance) {
     this.memberService = new MemberService(app)
+    this.userService = new UserService(app)
   }
 
   async createGuest(request: FastifyRequest<{ Querystring: { code?: string } }>, reply: FastifyReply) {
@@ -26,7 +37,8 @@ export class MemberController {
       return reply.code(201).send({
         user: {
           id: user.id,
-          name: user.name
+          name: user.name,
+          vipLevel: VipLevel.GUEST
         },
         token
       })
@@ -35,6 +47,130 @@ export class MemberController {
     }
   }
 
+  async upgradeGuest(request: FastifyRequest<{ Body: UpdateGuestBody }>, reply: FastifyReply) {
+    try {
+      const { userId, name, password, email, phone } = request.body
+
+      if (!name || !password) {
+        return reply.code(400).send({ message: '用户名和密码为必填字段' })
+      }
+      if (name.length < 3 || name.length > 20) {
+        return reply.code(400).send({ message: '用户名长度必须在3-20个字符之间' })
+      }
+      if (password.length < 6) {
+        return reply.code(400).send({ message: '密码长度不能少于6个字符' })
+      }
+      if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+        return reply.code(400).send({ message: '邮箱格式不正确' })
+      }
+      if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
+        return reply.code(400).send({ message: '手机号格式不正确' })
+      }
+
+      await this.memberService.upgradeGuest(userId, name, password, email, phone)
+
+      return reply.send({ message: '游客账户转换成功' })
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '更新游客失败'
+      if (errorMessage.includes('不存在')) {
+        return reply.code(404).send({ message: errorMessage })
+      } else if (errorMessage.includes('已被使用') || errorMessage.includes('格式')) {
+        return reply.code(400).send({ message: errorMessage })
+      }
+
+      return reply.code(500).send({ message: '更新游客失败', error: errorMessage })
+    }
+  }
+
+  async memberLogin(request: FastifyRequest<{ Body: MemberLoginBody }>, reply: FastifyReply) {
+    try {
+      const { name, password } = request.body
+
+      if (!name || !password) {
+        return reply.code(400).send({ message: '请输入用户名和密码' })
+      }
+      const loginResult = await this.memberService.validateMemberLogin(name, password)
+      if (!loginResult) {
+        return reply.code(401).send({ message: '用户名或密码错误' })
+      }
+      const { user, member } = loginResult
+      const token = await reply.jwtSign({ id: user.id, name: user.name, role: user.role })
+      await this.memberService.checkVipExpireTime(member)
+
+      return reply.send({
+        user: {
+          id: user.id,
+          name: user.name,
+          role: user.role,
+          vipLevel: member.vipLevel
+        },
+        token
+      })
+    } catch (error) {
+      return reply.code(500).send(error)
+    }
+  }
+
+  async profile(request: FastifyRequest, reply: FastifyReply) {
+    try {
+      const user = await this.userService.findById(request.user.id)
+      if (!user) {
+        return reply.code(404).send({ message: '会员信息不存在' })
+      }
+      const member = await this.memberService.findByUserId(user.id)
+      if (!member) {
+        return reply.code(404).send({ message: '会员信息不存在' })
+      }
+      await this.memberService.checkVipExpireTime(member)
+      return reply.send({
+        id: user.id,
+        name: user.name,
+        role: user.role,
+        vipLevel: member.vipLevel
+      })
+    } catch (error) {
+      return reply.code(500).send(error)
+    }
+  }
+
+  async resetPassword(request: FastifyRequest<{ Body: ResetPasswordBody }>, reply: FastifyReply) {
+    try {
+      const { password } = request.body
+      if (password.length < 8 || !/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
+        return reply.code(400).send({ message: '密码长度必须至少8位,包含大小写字母和数字' })
+      }
+
+      await this.userService.resetPassword(request.user.id, password)
+      return reply.send({ message: '密码重置成功' })
+    } catch (error) {
+      return reply.code(500).send(error)
+    }
+  }
+
+  async updateVipLevel(
+    request: FastifyRequest<{
+      Params: { id: string }
+      Body: { vipLevel: VipLevel; vipExpireTime?: Date }
+    }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const id = parseInt(request.params.id)
+      const { vipLevel, vipExpireTime } = request.body
+
+      const updatedMember = await this.memberService.updateVipLevel(id, vipLevel, vipExpireTime)
+
+      return reply.send({
+        member: {
+          vipLevel: updatedMember.vipLevel,
+          vipExpireTime: updatedMember.vipExpireTime
+        }
+      })
+    } catch (error) {
+      return reply.code(500).send({ message: '更新VIP等级失败', error })
+    }
+  }
+
   async create(request: FastifyRequest<{ Body: CreateMemberBody }>, reply: FastifyReply) {
     try {
       const { userId, email, phone, vipLevel, status, vipExpireTime } = request.body
@@ -223,38 +359,6 @@ export class MemberController {
     }
   }
 
-  async updateVipLevel(
-    request: FastifyRequest<{
-      Params: { id: string }
-      Body: { vipLevel: VipLevel; vipExpireTime?: Date }
-    }>,
-    reply: FastifyReply
-  ) {
-    try {
-      const id = parseInt(request.params.id)
-      const { vipLevel, vipExpireTime } = request.body
-
-      const updatedMember = await this.memberService.updateVipLevel(id, vipLevel, vipExpireTime)
-
-      return reply.send({
-        member: {
-          id: updatedMember.id,
-          userId: updatedMember.userId,
-          email: updatedMember.email,
-          phone: updatedMember.phone,
-          vipLevel: updatedMember.vipLevel,
-          status: updatedMember.status,
-          vipExpireTime: updatedMember.vipExpireTime,
-          lastLoginAt: updatedMember.lastLoginAt,
-          createdAt: updatedMember.createdAt,
-          updatedAt: updatedMember.updatedAt
-        }
-      })
-    } catch (error) {
-      return reply.code(500).send({ message: '更新VIP等级失败', error })
-    }
-  }
-
   async updateStatus(
     request: FastifyRequest<{
       Params: { id: string }

+ 17 - 0
src/dto/member.dto.ts

@@ -42,3 +42,20 @@ export interface MemberResponse {
   createdAt: Date
   updatedAt: Date
 }
+
+export interface UpdateGuestBody {
+  userId: number
+  name: string
+  password: string
+  email?: string
+  phone?: string
+}
+
+export interface MemberLoginBody {
+  name: string
+  password: string
+}
+
+export interface ResetPasswordBody {
+  password: string
+}

+ 4 - 1
src/entities/member.entity.ts

@@ -1,4 +1,4 @@
-import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, Index } from 'typeorm'
 
 export enum VipLevel {
   GUEST = 'guest',
@@ -16,6 +16,9 @@ export enum MemberStatus {
 }
 
 @Entity()
+@Index('idx_member_user_id', ['userId'])
+@Index('idx_member_vip_status', ['vipLevel', 'status'])
+@Index('idx_member_created_at', ['createdAt'])
 export class Member {
   @PrimaryGeneratedColumn()
   id: number

+ 37 - 10
src/routes/member.routes.ts

@@ -1,7 +1,14 @@
 import { FastifyInstance } from 'fastify'
 import { MemberController } from '../controllers/member.controller'
 import { authenticate, hasRole } from '../middlewares/auth.middleware'
-import { CreateMemberBody, UpdateMemberBody, ListMemberQuery } from '../dto/member.dto'
+import {
+  CreateMemberBody,
+  UpdateMemberBody,
+  ListMemberQuery,
+  UpdateGuestBody,
+  MemberLoginBody,
+  ResetPasswordBody
+} from '../dto/member.dto'
 import { VipLevel, MemberStatus } from '../entities/member.entity'
 import { UserRole } from '../entities/user.entity'
 
@@ -11,6 +18,32 @@ export default async function memberRoutes(fastify: FastifyInstance) {
   // 创建游客
   fastify.get<{ Querystring: { code?: string } }>('/guest', memberController.createGuest.bind(memberController))
 
+  // 游客更新为用户
+  fastify.post<{ Body: UpdateGuestBody }>(
+    '/guestUpgrade',
+    { onRequest: [authenticate] },
+    memberController.upgradeGuest.bind(memberController)
+  )
+
+  // 登录
+  fastify.post<{ Body: MemberLoginBody }>('/login', memberController.memberLogin.bind(memberController))
+
+  // 获取当前会员信息
+  fastify.get('/profile', { onRequest: [authenticate] }, memberController.profile.bind(memberController))
+
+  // 重置密码
+  fastify.post<{ Body: ResetPasswordBody }>(
+    '/reset-password',
+    { onRequest: [authenticate] },
+    memberController.resetPassword.bind(memberController)
+  )
+
+  // 更新VIP等级
+  fastify.patch<{
+    Params: { id: string }
+    Body: { vipLevel: VipLevel; vipExpireTime?: Date }
+  }>('/:id/vip-level', { onRequest: [hasRole(UserRole.ADMIN)] }, memberController.updateVipLevel.bind(memberController))
+
   // 创建会员
   fastify.post<{ Body: CreateMemberBody }>(
     '/',
@@ -28,7 +61,7 @@ export default async function memberRoutes(fastify: FastifyInstance) {
   // 根据用户ID获取会员信息
   fastify.get<{ Params: { userId: string } }>(
     '/user/:userId',
-    { onRequest: [authenticate] },
+    { onRequest: [hasRole(UserRole.ADMIN)] },
     memberController.getByUserId.bind(memberController)
   )
 
@@ -42,7 +75,7 @@ export default async function memberRoutes(fastify: FastifyInstance) {
   // 更新会员信息
   fastify.put<{ Body: UpdateMemberBody }>(
     '/',
-    { onRequest: [authenticate] },
+    { onRequest: [hasRole(UserRole.ADMIN)] },
     memberController.update.bind(memberController)
   )
 
@@ -53,15 +86,9 @@ export default async function memberRoutes(fastify: FastifyInstance) {
     memberController.delete.bind(memberController)
   )
 
-  // 获取所有会员(简化版)
+  // 获取所有会员
   fastify.get('/all', { onRequest: [hasRole(UserRole.ADMIN)] }, memberController.getAllMembers.bind(memberController))
 
-  // 更新VIP等级
-  fastify.patch<{
-    Params: { id: string }
-    Body: { vipLevel: VipLevel; vipExpireTime?: Date }
-  }>('/:id/vip-level', { onRequest: [authenticate] }, memberController.updateVipLevel.bind(memberController))
-
   // 更新会员状态
   fastify.patch<{
     Params: { id: string }

+ 87 - 9
src/services/member.service.ts

@@ -5,6 +5,7 @@ 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 bcrypt from 'bcryptjs'
 
 export class MemberService {
   private memberRepository: Repository<Member>
@@ -52,6 +53,85 @@ export class MemberService {
     })
   }
 
+  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
     email?: string
@@ -160,15 +240,6 @@ export class MemberService {
     })
   }
 
-  async findExpiredVipMembers(): Promise<Member[]> {
-    return this.memberRepository.find({
-      where: {
-        vipExpireTime: In([null, undefined]) ? undefined : undefined, // 这里需要更复杂的查询
-        vipLevel: In([VipLevel.DAILY, VipLevel.WEEKLY, VipLevel.MONTHLY, VipLevel.YEARLY])
-      }
-    })
-  }
-
   async countByVipLevel(): Promise<Record<VipLevel, number>> {
     const result: Record<VipLevel, number> = {} as Record<VipLevel, number>
 
@@ -190,4 +261,11 @@ export class MemberService {
 
     return result
   }
+
+  async checkVipExpireTime(member: Member): Promise<void> {
+    if (member.vipExpireTime && member.vipExpireTime < new Date()) {
+      member.vipLevel = VipLevel.GUEST
+      await this.update(member.id, member)
+    }
+  }
 }