feat: scope blacklists and paginate phone segments

This commit is contained in:
hectorzhao
2026-07-10 13:42:39 +08:00
parent ff0756ff9c
commit d5e668a2e6
18 changed files with 322 additions and 86 deletions
+51 -5
View File
@@ -9,6 +9,12 @@ export interface CreatePhoneSegmentDto {
city?: string;
}
export interface PhoneSegmentListQuery {
keyword?: string;
cursor?: string;
pageSize?: number;
}
export interface CreatePhoneCarrierRuleDto {
carrier: string;
pattern: string;
@@ -25,6 +31,7 @@ export interface CreateSensitiveWordDto {
export interface CreateBlacklistDto {
tenantId?: string;
applicationId?: string;
phoneNumber: string;
reason?: string;
status?: string;
@@ -48,6 +55,7 @@ export interface DictionaryStatusDto {
export interface DictionaryListQuery {
tenantId?: string;
applicationId?: string;
keyword?: string;
status?: string;
}
@@ -56,8 +64,34 @@ export interface DictionaryListQuery {
export class DictionariesService {
constructor(private readonly prisma: PrismaService) {}
listPhoneSegments() {
return this.prisma.phoneSegment.findMany({ orderBy: { prefix: 'asc' }, take: 200 });
async listPhoneSegments(query: PhoneSegmentListQuery = {}) {
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize ?? 20)));
const keyword = query.keyword?.trim();
const items = await this.prisma.phoneSegment.findMany({
where: {
AND: [
query.cursor ? { prefix: { gt: query.cursor } } : {},
keyword ? {
OR: [
{ prefix: { startsWith: keyword } },
{ carrier: { contains: keyword } },
{ province: { contains: keyword } },
{ city: { contains: keyword } },
],
} : {},
],
},
orderBy: { prefix: 'asc' },
take: pageSize + 1,
});
const hasMore = items.length > pageSize;
const pageItems = hasMore ? items.slice(0, pageSize) : items;
return {
items: pageItems,
pageSize,
hasMore,
nextCursor: hasMore ? pageItems.at(-1)?.prefix ?? null : null,
};
}
createPhoneSegment(data: CreatePhoneSegmentDto) {
@@ -161,24 +195,35 @@ export class DictionariesService {
return this.prisma.enterpriseBlacklist.findMany({
where: {
tenantId: query.tenantId,
applicationId: query.applicationId,
status: query.status && query.status !== 'all' ? query.status : undefined,
OR: query.keyword ? [
{ phoneNumber: { contains: query.keyword } },
{ reason: { contains: query.keyword } },
{ tenant: { name: { contains: query.keyword } } },
{ application: { name: { contains: query.keyword } } },
] : undefined,
},
include: { tenant: true },
include: { tenant: true, application: true },
orderBy: { createdAt: 'desc' },
take: 200,
});
}
async createEnterpriseBlacklist(data: CreateBlacklistDto) {
if (!data.tenantId) {
throw new BadRequestException('tenantId is required for enterprise blacklist');
if (!data.tenantId || !data.applicationId) {
throw new BadRequestException('tenantId and applicationId are required for application blacklist');
}
const application = await this.prisma.smsApplication.findUnique({
where: { id: data.applicationId },
select: { tenantId: true },
});
if (!application || application.tenantId !== data.tenantId) {
throw new BadRequestException('applicationId does not belong to tenantId');
}
const createData: Prisma.EnterpriseBlacklistUncheckedCreateInput = {
tenantId: data.tenantId,
applicationId: data.applicationId,
phoneNumber: data.phoneNumber,
reason: data.reason,
status: data.status ?? 'active',
@@ -186,6 +231,7 @@ export class DictionariesService {
const created = await this.prisma.enterpriseBlacklist.create({ data: createData });
await this.writeOperationLog(data.operatorId, 'enterprise_blacklist.create', 'enterprise_blacklist', created.id, {
tenantId: data.tenantId,
applicationId: data.applicationId,
phoneNumber: data.phoneNumber,
reason: data.reason,
});