import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; import type { AdminHqAccountsQueryDto } from './dto/admin-query.dto'; import type { CreateHqAccountDto, UpdateHqAccountDto } from './dto/admin-mutate.dto'; import { hashPassword } from '../../common/crypto/password.util'; function mapHqAccountRow(account: { id: bigint; phone: string; loginName: string | null; passwordHash: string | null; name: string; adminRole: string; status: string; lastLoginAt: Date | null; createdAt: Date; }) { return { id: account.id, phone: account.phone, loginName: account.loginName, hasPassword: !!account.passwordHash, name: account.name, adminRole: account.adminRole, status: account.status, lastLoginAt: account.lastLoginAt, createdAt: account.createdAt, }; } @Injectable() export class AdminHqAccountsService { constructor(private readonly prisma: PrismaService) {} async list(query: AdminHqAccountsQueryDto) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20; const where: Prisma.HqAccountWhereInput = {}; if (query.phone) where.phone = { contains: query.phone }; if (query.adminRole) where.adminRole = query.adminRole as Prisma.EnumHqAdminRoleFilter['equals']; if (query.status) where.status = query.status as Prisma.EnumAccountStatusFilter['equals']; const [items, total] = await Promise.all([ this.prisma.hqAccount.findMany({ where, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, select: { id: true, phone: true, loginName: true, passwordHash: true, name: true, adminRole: true, status: true, lastLoginAt: true, createdAt: true, }, }), this.prisma.hqAccount.count({ where }), ]); return serializeBigInt({ items: items.map(mapHqAccountRow), total, page, pageSize, }); } async detail(id: bigint) { const account = await this.prisma.hqAccount.findUnique({ where: { id }, select: { id: true, phone: true, loginName: true, passwordHash: true, name: true, adminRole: true, status: true, lastLoginAt: true, createdAt: true, }, }); if (!account) throw new NotFoundException('HQ 账号不存在'); return serializeBigInt(mapHqAccountRow(account)); } async create(dto: CreateHqAccountDto) { const adminRole = (dto.adminRole ?? 'OPS') as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE'; if (dto.credentialType === 'phone') { if (!dto.phone?.trim()) throw new BadRequestException('请填写手机号'); const phone = dto.phone.trim(); const exists = await this.prisma.hqAccount.findUnique({ where: { phone } }); if (exists) throw new BadRequestException('手机号已存在'); const account = await this.prisma.hqAccount.create({ data: { phone, name: dto.name, adminRole }, }); return serializeBigInt(mapHqAccountRow({ ...account, passwordHash: null })); } if (!dto.loginName?.trim() || !dto.password) { throw new BadRequestException('账号密码模式需填写用户名和密码'); } const loginName = dto.loginName.trim(); const loginTaken = await this.prisma.hqAccount.findUnique({ where: { loginName } }); if (loginTaken) throw new BadRequestException('用户名已存在'); const phone = dto.phone?.trim() || (await this.generatePlaceholderPhone()); const phoneTaken = await this.prisma.hqAccount.findUnique({ where: { phone } }); if (phoneTaken) throw new BadRequestException('手机号已存在'); const account = await this.prisma.hqAccount.create({ data: { phone, loginName, passwordHash: hashPassword(dto.password), name: dto.name, adminRole, }, }); return serializeBigInt(mapHqAccountRow(account)); } async update(id: bigint, dto: UpdateHqAccountDto) { const current = await this.prisma.hqAccount.findUnique({ where: { id } }); if (!current) throw new NotFoundException('HQ 账号不存在'); if (dto.loginName !== undefined) { const loginName = dto.loginName.trim(); if (!loginName) throw new BadRequestException('用户名不能为空'); const conflict = await this.prisma.hqAccount.findFirst({ where: { loginName, id: { not: id } }, }); if (conflict) throw new BadRequestException('用户名已存在'); } if (dto.phone !== undefined) { const phone = dto.phone.trim(); if (!/^1[3-9]\d{9}$/.test(phone)) { throw new BadRequestException('请输入正确的手机号'); } const phoneTaken = await this.prisma.hqAccount.findUnique({ where: { phone } }); if (phoneTaken && phoneTaken.id !== id) { throw new BadRequestException('手机号已存在'); } } const account = await this.prisma.hqAccount.update({ where: { id }, data: { ...(dto.name !== undefined ? { name: dto.name } : {}), ...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}), ...(dto.loginName !== undefined ? { loginName: dto.loginName.trim() } : {}), ...(dto.password ? { passwordHash: hashPassword(dto.password) } : {}), ...(dto.adminRole !== undefined ? { adminRole: dto.adminRole as 'SUPER_ADMIN' | 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE' } : {}), ...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}), }, select: { id: true, phone: true, loginName: true, passwordHash: true, name: true, adminRole: true, status: true, lastLoginAt: true, createdAt: true, }, }); return serializeBigInt(mapHqAccountRow(account)); } private async generatePlaceholderPhone(): Promise { for (let i = 0; i < 8; i += 1) { const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`.slice(-8); const phone = `199${suffix}`; const exists = await this.prisma.hqAccount.findUnique({ where: { phone } }); if (!exists) return phone; } throw new BadRequestException('无法生成占位手机号,请手动填写'); } }