@@ -1,11 +1,25 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { HqAdminRoleValue } from '@dukang/shared-types';
|
||||
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';
|
||||
|
||||
const HQ_ACCOUNT_SELECT = {
|
||||
id: true,
|
||||
phone: true,
|
||||
loginName: true,
|
||||
passwordHash: true,
|
||||
name: true,
|
||||
adminRole: true,
|
||||
status: true,
|
||||
lastLoginAt: true,
|
||||
createdAt: true,
|
||||
cities: { select: { cityId: true } },
|
||||
} satisfies Prisma.HqAccountSelect;
|
||||
|
||||
function mapHqAccountRow(account: {
|
||||
id: bigint;
|
||||
phone: string;
|
||||
@@ -16,6 +30,7 @@ function mapHqAccountRow(account: {
|
||||
status: string;
|
||||
lastLoginAt: Date | null;
|
||||
createdAt: Date;
|
||||
cities?: { cityId: bigint }[];
|
||||
}) {
|
||||
return {
|
||||
id: account.id,
|
||||
@@ -27,6 +42,7 @@ function mapHqAccountRow(account: {
|
||||
status: account.status,
|
||||
lastLoginAt: account.lastLoginAt,
|
||||
createdAt: account.createdAt,
|
||||
cityIds: (account.cities ?? []).map((c) => c.cityId.toString()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +50,31 @@ function mapHqAccountRow(account: {
|
||||
export class AdminHqAccountsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
private async assertCityIds(cityIds: string[]) {
|
||||
const unique = [...new Set(cityIds.map((id) => id.trim()).filter(Boolean))];
|
||||
if (!unique.length) return [] as bigint[];
|
||||
const ids = unique.map((id) => BigInt(id));
|
||||
const count = await this.prisma.commonCity.count({ where: { id: { in: ids } } });
|
||||
if (count !== ids.length) {
|
||||
throw new BadRequestException('存在无效城市');
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private assertCityRequirement(adminRole: string, cityIds: bigint[]) {
|
||||
if (adminRole === 'CITY_STORE_SERVICE' && !cityIds.length) {
|
||||
throw new BadRequestException('城市门店服务须至少勾选一个负责城市');
|
||||
}
|
||||
}
|
||||
|
||||
private async replaceCities(tx: Prisma.TransactionClient, hqAccountId: bigint, cityIds: bigint[]) {
|
||||
await tx.hqAccountCity.deleteMany({ where: { hqAccountId } });
|
||||
if (!cityIds.length) return;
|
||||
await tx.hqAccountCity.createMany({
|
||||
data: cityIds.map((cityId) => ({ hqAccountId, cityId })),
|
||||
});
|
||||
}
|
||||
|
||||
async list(query: AdminHqAccountsQueryDto) {
|
||||
const page = query.page ?? 1;
|
||||
const pageSize = query.pageSize ?? 20;
|
||||
@@ -48,17 +89,7 @@ export class AdminHqAccountsService {
|
||||
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,
|
||||
},
|
||||
select: HQ_ACCOUNT_SELECT,
|
||||
}),
|
||||
this.prisma.hqAccount.count({ where }),
|
||||
]);
|
||||
@@ -73,34 +104,31 @@ export class AdminHqAccountsService {
|
||||
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,
|
||||
},
|
||||
select: HQ_ACCOUNT_SELECT,
|
||||
});
|
||||
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';
|
||||
const adminRole = (dto.adminRole ?? 'OPS') as HqAdminRoleValue;
|
||||
const cityIds = await this.assertCityIds(dto.cityIds ?? []);
|
||||
this.assertCityRequirement(adminRole, cityIds);
|
||||
|
||||
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 },
|
||||
const account = await this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.hqAccount.create({
|
||||
data: { phone, name: dto.name, adminRole },
|
||||
select: HQ_ACCOUNT_SELECT,
|
||||
});
|
||||
await this.replaceCities(tx, created.id, cityIds);
|
||||
return tx.hqAccount.findUniqueOrThrow({ where: { id: created.id }, select: HQ_ACCOUNT_SELECT });
|
||||
});
|
||||
return serializeBigInt(mapHqAccountRow({ ...account, passwordHash: null }));
|
||||
return serializeBigInt(mapHqAccountRow(account));
|
||||
}
|
||||
|
||||
if (!dto.loginName?.trim() || !dto.password) {
|
||||
@@ -114,27 +142,34 @@ export class AdminHqAccountsService {
|
||||
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,
|
||||
},
|
||||
const account = await this.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.hqAccount.create({
|
||||
data: {
|
||||
phone,
|
||||
loginName,
|
||||
passwordHash: hashPassword(dto.password!),
|
||||
name: dto.name,
|
||||
adminRole,
|
||||
},
|
||||
select: HQ_ACCOUNT_SELECT,
|
||||
});
|
||||
await this.replaceCities(tx, created.id, cityIds);
|
||||
return tx.hqAccount.findUniqueOrThrow({ where: { id: created.id }, select: HQ_ACCOUNT_SELECT });
|
||||
});
|
||||
return serializeBigInt(mapHqAccountRow(account));
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateHqAccountDto) {
|
||||
const current = await this.prisma.hqAccount.findUnique({ where: { id } });
|
||||
const current = await this.prisma.hqAccount.findUnique({
|
||||
where: { id },
|
||||
include: { cities: { select: { cityId: true } } },
|
||||
});
|
||||
if (!current) throw new NotFoundException('HQ 账号不存在');
|
||||
|
||||
if (dto.loginName !== undefined) {
|
||||
const loginName = dto.loginName.trim();
|
||||
if (!loginName) throw new BadRequestException('用户名不能为空');
|
||||
const loginNameInput = dto.loginName === undefined ? undefined : dto.loginName.trim();
|
||||
if (loginNameInput) {
|
||||
const conflict = await this.prisma.hqAccount.findFirst({
|
||||
where: { loginName, id: { not: id } },
|
||||
where: { loginName: loginNameInput, id: { not: id } },
|
||||
});
|
||||
if (conflict) throw new BadRequestException('用户名已存在');
|
||||
}
|
||||
@@ -150,29 +185,34 @@ export class AdminHqAccountsService {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
const nextRole = (dto.adminRole ?? current.adminRole) as HqAdminRoleValue;
|
||||
const nextCityIds =
|
||||
dto.cityIds !== undefined
|
||||
? await this.assertCityIds(dto.cityIds)
|
||||
: current.cities.map((c) => c.cityId);
|
||||
this.assertCityRequirement(nextRole, nextCityIds);
|
||||
|
||||
const roleChanged = dto.adminRole !== undefined && dto.adminRole !== current.adminRole;
|
||||
|
||||
const account = await this.prisma.$transaction(async (tx) => {
|
||||
await tx.hqAccount.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(dto.name !== undefined ? { name: dto.name } : {}),
|
||||
...(dto.phone !== undefined ? { phone: dto.phone.trim() } : {}),
|
||||
...(loginNameInput ? { loginName: loginNameInput } : {}),
|
||||
...(dto.password ? { passwordHash: hashPassword(dto.password) } : {}),
|
||||
...(dto.adminRole !== undefined ? { adminRole: nextRole } : {}),
|
||||
...(dto.status !== undefined ? { status: dto.status as 'ACTIVE' | 'DISABLED' } : {}),
|
||||
},
|
||||
});
|
||||
if (dto.cityIds !== undefined) {
|
||||
await this.replaceCities(tx, id, nextCityIds);
|
||||
}
|
||||
if (roleChanged) {
|
||||
await tx.hqAccountPermission.deleteMany({ where: { hqAccountId: id } });
|
||||
}
|
||||
return tx.hqAccount.findUniqueOrThrow({ where: { id }, select: HQ_ACCOUNT_SELECT });
|
||||
});
|
||||
return serializeBigInt(mapHqAccountRow(account));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user