Files
dukang/server/dukang-api/src/modules/ops/admin-hq-accounts.service.ts
T
jacy 5935024ea8
CI / verify (pull_request) Has been cancelled
v3.5.8和v3.5.9版本更新
2026-08-25 09:20:32 +08:00

230 lines
8.5 KiB
TypeScript

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;
loginName: string | null;
passwordHash: string | null;
name: string;
adminRole: string;
status: string;
lastLoginAt: Date | null;
createdAt: Date;
cities?: { cityId: bigint }[];
}) {
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,
cityIds: (account.cities ?? []).map((c) => c.cityId.toString()),
};
}
@Injectable()
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;
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: HQ_ACCOUNT_SELECT,
}),
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: HQ_ACCOUNT_SELECT,
});
if (!account) throw new NotFoundException('HQ 账号不存在');
return serializeBigInt(mapHqAccountRow(account));
}
async create(dto: CreateHqAccountDto) {
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.$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));
}
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.$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 },
include: { cities: { select: { cityId: true } } },
});
if (!current) throw new NotFoundException('HQ 账号不存在');
const loginNameInput = dto.loginName === undefined ? undefined : dto.loginName.trim();
if (loginNameInput) {
const conflict = await this.prisma.hqAccount.findFirst({
where: { loginName: loginNameInput, 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 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));
}
private async generatePlaceholderPhone(): Promise<string> {
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('无法生成占位手机号,请手动填写');
}
}