feat: add reconciliation and quality reporting
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
CreateCommonReportFieldDto,
|
||||
CreateBlacklistDto,
|
||||
CreateDrainageFieldDto,
|
||||
CreatePhoneCarrierRuleDto,
|
||||
@@ -118,4 +119,19 @@ export class DictionariesController {
|
||||
deleteDrainageField(@Param('id') id: string) {
|
||||
return this.dictionaries.deleteDrainageField(id);
|
||||
}
|
||||
|
||||
@Get('common-report-fields')
|
||||
listCommonReportFields() {
|
||||
return this.dictionaries.listCommonReportFields();
|
||||
}
|
||||
|
||||
@Post('common-report-fields')
|
||||
createCommonReportField(@Body() body: CreateCommonReportFieldDto) {
|
||||
return this.dictionaries.createCommonReportField(body);
|
||||
}
|
||||
|
||||
@Delete('common-report-fields/:id')
|
||||
deleteCommonReportField(@Param('id') id: string) {
|
||||
return this.dictionaries.deleteCommonReportField(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,20 @@ function createPrismaMock() {
|
||||
},
|
||||
drainageField: {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'field-1', ...data })),
|
||||
delete: jest.fn().mockResolvedValue({ id: 'field-1' }),
|
||||
},
|
||||
channelReportField: {
|
||||
count: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
commonReportField: {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
count: jest.fn().mockResolvedValue(0),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'common-1', ...data })),
|
||||
delete: jest.fn().mockResolvedValue({ id: 'common-1' }),
|
||||
},
|
||||
smsApplication: {
|
||||
findUnique: jest.fn().mockResolvedValue({ id: 'app-1', tenantId: 'tenant-1' }),
|
||||
},
|
||||
@@ -55,14 +63,38 @@ describe('DictionariesService', () => {
|
||||
|
||||
it('returns drainage field usage counts and blocks deleting fields used by channels', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.drainageField.findMany.mockResolvedValue([{ id: 'field-1', code: 'license', _count: { channelReportFields: 2 } }]);
|
||||
prisma.drainageField.findMany.mockResolvedValue([{ id: 'field-1', code: 'license', _count: { channelReportFields: 2, commonReportFields: 0 } }]);
|
||||
prisma.channelReportField.count.mockResolvedValue(2);
|
||||
const service = new DictionariesService(prisma as never);
|
||||
|
||||
await expect(service.listDrainageFields()).resolves.toEqual([{ id: 'field-1', code: 'license', usageCount: 2 }]);
|
||||
await expect(service.listDrainageFields()).resolves.toEqual([{ id: 'field-1', code: 'license', usageCount: 2, commonUsageCount: 0 }]);
|
||||
await expect(service.deleteDrainageField('field-1')).rejects.toThrow('不能删除');
|
||||
expect(prisma.drainageField.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates and deletes real common signature and drainage field configurations', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.drainageField.findUnique = jest.fn().mockResolvedValue({ id: 'field-1', code: 'license', required: false, status: 'active' });
|
||||
const service = new DictionariesService(prisma as never);
|
||||
|
||||
await service.createCommonReportField({ drainageFieldId: 'field-1', reportType: 'signature', required: true });
|
||||
await service.deleteCommonReportField('common-1');
|
||||
|
||||
expect(prisma.commonReportField.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ drainageFieldId: 'field-1', reportType: 'signature', required: true, status: 'active' }),
|
||||
include: { drainageField: true },
|
||||
});
|
||||
expect(prisma.commonReportField.delete).toHaveBeenCalledWith({ where: { id: 'common-1' } });
|
||||
});
|
||||
|
||||
it('blocks deleting a field referenced by a common configuration', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.commonReportField.count.mockResolvedValue(1);
|
||||
const service = new DictionariesService(prisma as never);
|
||||
|
||||
await expect(service.deleteDrainageField('field-1')).rejects.toThrow('通用配置');
|
||||
expect(prisma.drainageField.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
it('paginates phone segments with a real database count', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.phoneSegment.findMany.mockResolvedValue([
|
||||
|
||||
@@ -53,6 +53,13 @@ export interface CreateDrainageFieldDto {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateCommonReportFieldDto {
|
||||
drainageFieldId: string;
|
||||
reportType: 'signature' | 'drainage';
|
||||
required?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface DictionaryStatusDto {
|
||||
status?: string;
|
||||
operatorId?: string;
|
||||
@@ -264,10 +271,14 @@ export class DictionariesService {
|
||||
|
||||
async listDrainageFields() {
|
||||
const fields = await this.prisma.drainageField.findMany({
|
||||
include: { _count: { select: { channelReportFields: true } } },
|
||||
include: { _count: { select: { channelReportFields: true, commonReportFields: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
return fields.map(({ _count, ...field }) => ({ ...field, usageCount: _count.channelReportFields }));
|
||||
return fields.map(({ _count, ...field }) => ({
|
||||
...field,
|
||||
usageCount: _count.channelReportFields,
|
||||
commonUsageCount: _count.commonReportFields,
|
||||
}));
|
||||
}
|
||||
|
||||
createDrainageField(data: CreateDrainageFieldDto) {
|
||||
@@ -291,13 +302,53 @@ export class DictionariesService {
|
||||
}
|
||||
|
||||
async deleteDrainageField(id: string) {
|
||||
const usageCount = await this.prisma.channelReportField.count({ where: { drainageFieldId: id } });
|
||||
if (usageCount > 0) {
|
||||
throw new BadRequestException(`该字段已被 ${usageCount} 个通道使用,不能删除`);
|
||||
const [usageCount, commonUsageCount] = await Promise.all([
|
||||
this.prisma.channelReportField.count({ where: { drainageFieldId: id } }),
|
||||
this.prisma.commonReportField.count({ where: { drainageFieldId: id } }),
|
||||
]);
|
||||
if (usageCount > 0 || commonUsageCount > 0) {
|
||||
throw new BadRequestException(`该字段已被 ${usageCount} 个通道和 ${commonUsageCount} 个通用配置使用,不能删除`);
|
||||
}
|
||||
return this.prisma.drainageField.delete({ where: { id } });
|
||||
}
|
||||
|
||||
listCommonReportFields() {
|
||||
return this.prisma.commonReportField.findMany({
|
||||
include: { drainageField: true },
|
||||
orderBy: [{ reportType: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
async createCommonReportField(data: CreateCommonReportFieldDto) {
|
||||
if (data.reportType !== 'signature' && data.reportType !== 'drainage') {
|
||||
throw new BadRequestException('reportType must be signature or drainage');
|
||||
}
|
||||
const field = await this.prisma.drainageField.findUnique({ where: { id: data.drainageFieldId } });
|
||||
if (!field || field.status !== 'active') {
|
||||
throw new BadRequestException('报备字段库字段不存在或已停用');
|
||||
}
|
||||
const existing = await this.prisma.commonReportField.findUnique({
|
||||
where: { drainageFieldId_reportType: { drainageFieldId: field.id, reportType: data.reportType } },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BadRequestException('该字段已配置为对应类型的通用字段');
|
||||
}
|
||||
return this.prisma.commonReportField.create({
|
||||
data: {
|
||||
drainageFieldId: field.id,
|
||||
reportType: data.reportType,
|
||||
required: data.required ?? field.required,
|
||||
sortOrder: data.sortOrder ?? 100,
|
||||
status: 'active',
|
||||
},
|
||||
include: { drainageField: true },
|
||||
});
|
||||
}
|
||||
|
||||
deleteCommonReportField(id: string) {
|
||||
return this.prisma.commonReportField.delete({ where: { id } });
|
||||
}
|
||||
|
||||
private writeOperationLog(userId: string | undefined, action: string, resource: string, resourceId: string, detail: Record<string, unknown>) {
|
||||
return this.prisma.operationLog.create({
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user