Selaa lähdekoodia

更新用户控制器和服务,新增用户密码验证逻辑,调整用户实体以支持可选密码字段,同时更新应用路由以支持新的成员路由。

wuyi 3 kuukautta sitten
vanhempi
commit
4815a798b2

+ 3 - 1
src/app.ts

@@ -17,6 +17,7 @@ import teamRoutes from './routes/team.routes'
 import teamMembersRoutes from './routes/team-members.routes'
 import teamMembersRoutes from './routes/team-members.routes'
 import promotionLinkRoutes from './routes/promotion-link.routes'
 import promotionLinkRoutes from './routes/promotion-link.routes'
 import paymentRoutes from './routes/payment.routes'
 import paymentRoutes from './routes/payment.routes'
+import memberRoutes from './routes/member.routes'
 
 
 const options: FastifyEnvOptions = {
 const options: FastifyEnvOptions = {
   schema: schema,
   schema: schema,
@@ -86,9 +87,10 @@ export const createApp = async () => {
   app.register(incomeRecordsRoutes, { prefix: '/api/income' })
   app.register(incomeRecordsRoutes, { prefix: '/api/income' })
   app.register(financeRoutes, { prefix: '/api/finance' })
   app.register(financeRoutes, { prefix: '/api/finance' })
   app.register(teamRoutes, { prefix: '/api/teams' })
   app.register(teamRoutes, { prefix: '/api/teams' })
-  app.register(teamMembersRoutes, { prefix: '/api/members' })
+  app.register(teamMembersRoutes, { prefix: '/api/team-members' })
   app.register(promotionLinkRoutes, { prefix: '/api/links' })
   app.register(promotionLinkRoutes, { prefix: '/api/links' })
   app.register(paymentRoutes, { prefix: '/api/payment' })
   app.register(paymentRoutes, { prefix: '/api/payment' })
+  app.register(memberRoutes, { prefix: '/api/member' })
 
 
   const dataSource = createDataSource(app)
   const dataSource = createDataSource(app)
   await dataSource.initialize()
   await dataSource.initialize()

+ 306 - 0
src/controllers/member.controller.ts

@@ -0,0 +1,306 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { MemberService } from '../services/member.service'
+import { CreateMemberBody, UpdateMemberBody, ListMemberQuery, MemberResponse } from '../dto/member.dto'
+import { VipLevel, MemberStatus } from '../entities/member.entity'
+
+export class MemberController {
+  private memberService: MemberService
+
+  constructor(app: FastifyInstance) {
+    this.memberService = new MemberService(app)
+  }
+
+  async createGuest(request: FastifyRequest<{ Querystring: { code?: string } }>, reply: FastifyReply) {
+    try {
+      const { code } = request.query || {}
+      
+      // 获取客户端IP地址
+      const ip = request.ip || 
+                 request.headers['x-forwarded-for'] as string || 
+                 request.headers['x-real-ip'] as string || 
+                 'unknown'
+      
+      await this.memberService.createGuest(code, ip)
+      return reply.code(201).send({ message: '创建游客成功' })
+    } catch (error) {
+      return reply.code(500).send({ message: '创建游客失败', error })
+    }
+  }
+
+  async create(request: FastifyRequest<{ Body: CreateMemberBody }>, reply: FastifyReply) {
+    try {
+      const { userId, email, phone, vipLevel, status, vipExpireTime } = request.body
+
+      // 检查用户是否已存在
+      const existingMember = await this.memberService.findByUserId(userId)
+      if (existingMember) {
+        return reply.code(400).send({ message: '该用户已经是会员' })
+      }
+
+      // 检查邮箱是否已存在
+      if (email) {
+        const existingEmail = await this.memberService.findByEmail(email)
+        if (existingEmail) {
+          return reply.code(400).send({ message: '邮箱已被使用' })
+        }
+      }
+
+      // 检查手机号是否已存在
+      if (phone) {
+        const existingPhone = await this.memberService.findByPhone(phone)
+        if (existingPhone) {
+          return reply.code(400).send({ message: '手机号已被使用' })
+        }
+      }
+
+      const member = await this.memberService.create({
+        userId,
+        email,
+        phone,
+        vipLevel,
+        status,
+        vipExpireTime
+      })
+
+      return reply.code(201).send({ message: '创建会员成功' })
+    } catch (error) {
+      return reply.code(500).send({ message: '创建会员失败', error })
+    }
+  }
+
+  async getById(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const id = parseInt(request.params.id)
+      const member = await this.memberService.findById(id)
+
+      return reply.send({
+        member: {
+          id: member.id,
+          userId: member.userId,
+          email: member.email,
+          phone: member.phone,
+          vipLevel: member.vipLevel,
+          status: member.status,
+          vipExpireTime: member.vipExpireTime,
+          lastLoginAt: member.lastLoginAt,
+          createdAt: member.createdAt,
+          updatedAt: member.updatedAt
+        }
+      })
+    } catch (error) {
+      return reply.code(404).send({ message: '会员不存在' })
+    }
+  }
+
+  async getByUserId(request: FastifyRequest<{ Params: { userId: string } }>, reply: FastifyReply) {
+    try {
+      const userId = parseInt(request.params.userId)
+      const member = await this.memberService.findByUserId(userId)
+
+      if (!member) {
+        return reply.code(404).send({ message: '会员不存在' })
+      }
+
+      return reply.send({
+        member: {
+          id: member.id,
+          userId: member.userId,
+          email: member.email,
+          phone: member.phone,
+          vipLevel: member.vipLevel,
+          status: member.status,
+          vipExpireTime: member.vipExpireTime,
+          lastLoginAt: member.lastLoginAt,
+          createdAt: member.createdAt,
+          updatedAt: member.updatedAt
+        }
+      })
+    } catch (error) {
+      return reply.code(500).send({ message: '查询会员失败', error })
+    }
+  }
+
+  async list(request: FastifyRequest<{ Querystring: ListMemberQuery }>, reply: FastifyReply) {
+    try {
+      const { page, size, vipLevel, status, userId } = request.query
+
+      const result = await this.memberService.list(page || 0, size || 20, { vipLevel, status, userId })
+
+      return reply.send(result)
+    } catch (error) {
+      return reply.code(500).send({ message: '查询会员列表失败', error })
+    }
+  }
+
+  async update(request: FastifyRequest<{ Body: UpdateMemberBody }>, reply: FastifyReply) {
+    try {
+      const { id, userId, email, phone, vipLevel, status, vipExpireTime } = request.body
+
+      // 检查会员是否存在
+      try {
+        await this.memberService.findById(id)
+      } catch (error) {
+        return reply.code(404).send({ message: '会员不存在' })
+      }
+
+      // 检查邮箱是否已被其他会员使用
+      if (email) {
+        const existingEmail = await this.memberService.findByEmail(email)
+        if (existingEmail && existingEmail.id !== id) {
+          return reply.code(400).send({ message: '邮箱已被其他会员使用' })
+        }
+      }
+
+      // 检查手机号是否已被其他会员使用
+      if (phone) {
+        const existingPhone = await this.memberService.findByPhone(phone)
+        if (existingPhone && existingPhone.id !== id) {
+          return reply.code(400).send({ message: '手机号已被其他会员使用' })
+        }
+      }
+
+      const updatedMember = await this.memberService.update(id, {
+        userId,
+        email,
+        phone,
+        vipLevel,
+        status,
+        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: '更新会员失败', error })
+    }
+  }
+
+  async delete(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const id = parseInt(request.params.id)
+
+      // 检查会员是否存在
+      try {
+        await this.memberService.findById(id)
+      } catch (error) {
+        return reply.code(404).send({ message: '会员不存在' })
+      }
+
+      await this.memberService.delete(id)
+
+      return reply.send({ message: '删除会员成功' })
+    } catch (error) {
+      return reply.code(500).send({ message: '删除会员失败', error })
+    }
+  }
+
+  async getAllMembers(request: FastifyRequest, reply: FastifyReply) {
+    try {
+      const members = await this.memberService.findAllMembers()
+      return reply.send({ members })
+    } catch (error) {
+      return reply.code(500).send({ message: '获取所有会员失败', 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: {
+          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 }
+      Body: { status: MemberStatus }
+    }>,
+    reply: FastifyReply
+  ) {
+    try {
+      const id = parseInt(request.params.id)
+      const { status } = request.body
+
+      const updatedMember = await this.memberService.updateStatus(id, status)
+
+      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: '更新会员状态失败', error })
+    }
+  }
+
+  async updateLastLogin(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const id = parseInt(request.params.id)
+      await this.memberService.updateLastLogin(id)
+
+      return reply.send({ message: '更新最后登录时间成功' })
+    } catch (error) {
+      return reply.code(500).send({ message: '更新最后登录时间失败', error })
+    }
+  }
+
+  async getStatistics(request: FastifyRequest, reply: FastifyReply) {
+    try {
+      const vipLevelStats = await this.memberService.countByVipLevel()
+      const statusStats = await this.memberService.countByStatus()
+
+      return reply.send({
+        vipLevelStats,
+        statusStats
+      })
+    } catch (error) {
+      return reply.code(500).send({ message: '获取统计数据失败', error })
+    }
+  }
+}

+ 4 - 0
src/controllers/user.controller.ts

@@ -52,6 +52,10 @@ export class UserController {
         return reply.code(401).send({ message: '用户名或密码错误' })
         return reply.code(401).send({ message: '用户名或密码错误' })
       }
       }
 
 
+      if (!user.password) {
+        return reply.code(401).send({ message: '该用户未设置密码,请联系管理员' })
+      }
+
       const isValidPassword = await this.userService.validatePassword(user, password)
       const isValidPassword = await this.userService.validatePassword(user, password)
       if (!isValidPassword) {
       if (!isValidPassword) {
         return reply.code(401).send({ message: '用户名或密码错误' })
         return reply.code(401).send({ message: '用户名或密码错误' })

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

@@ -0,0 +1,44 @@
+import { FastifyRequest } from 'fastify'
+import { VipLevel, MemberStatus } from '../entities/member.entity'
+import { Pagination } from './common.dto'
+
+export interface CreateMemberBody {
+  userId: number
+  email?: string
+  phone?: string
+  vipLevel?: VipLevel
+  status?: MemberStatus
+  vipExpireTime?: Date
+  ip?: string
+}
+
+export interface UpdateMemberBody {
+  id: number
+  userId?: number
+  email?: string
+  phone?: string
+  vipLevel?: VipLevel
+  status?: MemberStatus
+  vipExpireTime?: Date
+  ip?: string
+}
+
+export interface ListMemberQuery extends Pagination {
+  vipLevel?: VipLevel | VipLevel[]
+  status?: MemberStatus | MemberStatus[]
+  userId?: number
+}
+
+export interface MemberResponse {
+  id: number
+  userId: number
+  email?: string
+  phone?: string
+  vipLevel: VipLevel
+  status: MemberStatus
+  vipExpireTime?: Date
+  lastLoginAt?: Date
+  ip?: string
+  createdAt: Date
+  updatedAt: Date
+}

+ 60 - 0
src/entities/member.entity.ts

@@ -0,0 +1,60 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+
+export enum VipLevel {
+  GUEST = 'guest',
+  FREE = 'free',
+  DAILY = 'daily',
+  WEEKLY = 'weekly',
+  MONTHLY = 'monthly',
+  YEARLY = 'yearly',
+  LIFETIME = 'lifetime'
+}
+
+export enum MemberStatus {
+  ACTIVE = 'active',
+  INACTIVE = 'inactive'
+}
+
+@Entity()
+export class Member {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column()
+  userId: number
+
+  @Column({ unique: true, length: 100, nullable: true })
+  email: string
+
+  @Column({ unique: true, length: 20, nullable: true })
+  phone: string
+
+  @Column({ nullable: true, length: 45 })
+  ip: string
+
+  @Column({
+    type: 'enum',
+    enum: VipLevel,
+    default: VipLevel.GUEST
+  })
+  vipLevel: VipLevel
+
+  @Column({
+    type: 'enum',
+    enum: MemberStatus,
+    default: MemberStatus.ACTIVE
+  })
+  status: MemberStatus
+
+  @Column({ type: 'datetime', nullable: true })
+  vipExpireTime: Date
+
+  @Column({ type: 'datetime', nullable: true })
+  lastLoginAt: Date
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+}

+ 1 - 1
src/entities/user.entity.ts

@@ -12,7 +12,7 @@ export class User {
   @PrimaryGeneratedColumn()
   @PrimaryGeneratedColumn()
   id: number
   id: number
 
 
-  @Column()
+  @Column({ nullable: true })
   password: string
   password: string
 
 
   @Column({ unique: true, length: 100 })
   @Column({ unique: true, length: 100 })

+ 84 - 0
src/routes/member.routes.ts

@@ -0,0 +1,84 @@
+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 { VipLevel, MemberStatus } from '../entities/member.entity'
+import { UserRole } from '../entities/user.entity'
+
+export default async function memberRoutes(fastify: FastifyInstance) {
+  const memberController = new MemberController(fastify)
+
+  // 创建游客
+  fastify.get<{ Querystring: { code?: string } }>('/guest', memberController.createGuest.bind(memberController))
+
+  // 创建会员
+  fastify.post<{ Body: CreateMemberBody }>(
+    '/',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    memberController.create.bind(memberController)
+  )
+
+  // 根据ID获取会员信息
+  fastify.get<{ Params: { id: string } }>(
+    '/:id',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    memberController.getById.bind(memberController)
+  )
+
+  // 根据用户ID获取会员信息
+  fastify.get<{ Params: { userId: string } }>(
+    '/user/:userId',
+    { onRequest: [authenticate] },
+    memberController.getByUserId.bind(memberController)
+  )
+
+  // 获取会员列表(支持分页和筛选)
+  fastify.get<{ Querystring: ListMemberQuery }>(
+    '/',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    memberController.list.bind(memberController)
+  )
+
+  // 更新会员信息
+  fastify.put<{ Body: UpdateMemberBody }>(
+    '/',
+    { onRequest: [authenticate] },
+    memberController.update.bind(memberController)
+  )
+
+  // 删除会员
+  fastify.delete<{ Params: { id: string } }>(
+    '/:id',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    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 }
+    Body: { status: MemberStatus }
+  }>('/:id/status', { onRequest: [hasRole(UserRole.ADMIN)] }, memberController.updateStatus.bind(memberController))
+
+  // 更新最后登录时间
+  fastify.patch<{ Params: { id: string } }>(
+    '/:id/last-login',
+    { onRequest: [authenticate] },
+    memberController.updateLastLogin.bind(memberController)
+  )
+
+  // 获取会员统计数据
+  fastify.get(
+    '/statistics',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    memberController.getStatistics.bind(memberController)
+  )
+}

+ 193 - 0
src/services/member.service.ts

@@ -0,0 +1,193 @@
+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'
+
+export class MemberService {
+  private memberRepository: Repository<Member>
+  private userRepository: Repository<User>
+  private dataSource: DataSource
+
+  constructor(app: FastifyInstance) {
+    this.memberRepository = app.dataSource.getRepository(Member)
+    this.userRepository = app.dataSource.getRepository(User)
+    this.dataSource = app.dataSource
+  }
+
+  async createGuest(code?: string, ip?: string): Promise<{ user: User; member: Member }> {
+    return await this.dataSource.transaction(async manager => {
+      const randomSuffix = Math.random().toString(36).substring(2, 10)
+      const guestName = `m_${randomSuffix}`
+
+      let finalGuestName = guestName
+      let counter = 0
+      while (await manager.findOne(User, { where: { name: finalGuestName } })) {
+        counter++
+        finalGuestName = `${guestName}${counter}`
+      }
+
+      // 后续校验推广码是哪个团队
+      const parentId = code ? parseInt(code) : 1
+
+      const user = manager.create(User, {
+        name: finalGuestName,
+        role: UserRole.USER,
+        parentId
+      })
+      const savedUser = await manager.save(user)
+      const member = manager.create(Member, {
+        userId: savedUser.id,
+        vipLevel: VipLevel.GUEST,
+        status: MemberStatus.ACTIVE,
+        ip: ip || 'unknown',
+        lastLoginAt: new Date()
+      })
+      const savedMember = await manager.save(member)
+
+      return {
+        user: savedUser,
+        member: savedMember
+      }
+    })
+  }
+
+  async create(data: {
+    userId: number
+    email?: string
+    phone?: string
+    vipLevel?: VipLevel
+    status?: MemberStatus
+    vipExpireTime?: Date
+    ip?: string
+  }): Promise<Member> {
+    const member = this.memberRepository.create({
+      userId: data.userId,
+      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
+    }
+    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 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>
+
+    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
+  }
+}

+ 17 - 5
src/services/user.service.ts

@@ -11,14 +11,23 @@ export class UserService {
     this.userRepository = app.dataSource.getRepository(User)
     this.userRepository = app.dataSource.getRepository(User)
   }
   }
 
 
-  async create(password: string, name: string, role: UserRole = UserRole.USER, parentId?: number): Promise<User> {
-    const hashedPassword = await bcrypt.hash(password, 10)
-    const user = this.userRepository.create({
+  async create(
+    password: string | null,
+    name: string,
+    role: UserRole = UserRole.USER,
+    parentId?: number
+  ): Promise<User> {
+    const userData: Partial<User> = {
       name,
       name,
-      password: hashedPassword,
       role,
       role,
       parentId
       parentId
-    })
+    }
+
+    if (password) {
+      userData.password = await bcrypt.hash(password, 10)
+    }
+
+    const user = this.userRepository.create(userData)
     return this.userRepository.save(user)
     return this.userRepository.save(user)
   }
   }
 
 
@@ -31,6 +40,9 @@ export class UserService {
   }
   }
 
 
   async validatePassword(user: User, password: string): Promise<boolean> {
   async validatePassword(user: User, password: string): Promise<boolean> {
+    if (!user.password) {
+      return false
+    }
     return bcrypt.compare(password, user.password)
     return bcrypt.compare(password, user.password)
   }
   }