596 lines
22 KiB
TypeScript
596 lines
22 KiB
TypeScript
import { BadRequestException, ConflictException, Injectable, NotFoundException, Optional } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { PhoneRoutingLookupService } from './phone-routing-lookup.service';
|
|
import {
|
|
detectDrainageContentWithRules,
|
|
invalidateDrainageDetectionRuleCache,
|
|
validateDrainageDetectionPattern,
|
|
} from '../send-chain/drainage-content-detection';
|
|
|
|
export interface CreatePhoneSegmentDto {
|
|
prefix: string;
|
|
carrier: string;
|
|
province?: string;
|
|
city?: string;
|
|
}
|
|
|
|
function visibleDictionaryStatus(status?: string): string | Prisma.StringFilter {
|
|
return status && status !== 'all' && status !== 'deleted' ? status : { not: 'deleted' };
|
|
}
|
|
|
|
export interface PhoneSegmentListQuery {
|
|
keyword?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
export interface CreatePhoneCarrierRuleDto {
|
|
carrier: string;
|
|
pattern: string;
|
|
priority?: number;
|
|
status?: string;
|
|
remark?: string;
|
|
}
|
|
|
|
export interface PageQuery {
|
|
keyword?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}
|
|
|
|
export interface CreateSensitiveWordDto {
|
|
word: string;
|
|
level?: string;
|
|
status?: string;
|
|
}
|
|
|
|
export interface CreateBlacklistDto {
|
|
tenantId?: string;
|
|
applicationId?: string;
|
|
phoneNumber: string;
|
|
reason?: string;
|
|
status?: string;
|
|
operatorId?: string;
|
|
}
|
|
|
|
export interface CreateDrainageFieldDto {
|
|
code: string;
|
|
name: string;
|
|
fieldType: 'string' | 'image' | 'file';
|
|
required?: boolean;
|
|
status?: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface UpsertDrainageDetectionRuleDto {
|
|
code: string;
|
|
name: string;
|
|
category: 'url' | 'mobile' | 'landline';
|
|
pattern: string;
|
|
flags?: string;
|
|
priority?: number;
|
|
status?: string;
|
|
description?: string;
|
|
operatorId?: string;
|
|
}
|
|
|
|
export interface TestDrainageDetectionDto {
|
|
content: string;
|
|
rule?: UpsertDrainageDetectionRuleDto;
|
|
}
|
|
|
|
export interface CreateCommonReportFieldDto {
|
|
drainageFieldId: string;
|
|
reportType: 'signature' | 'drainage';
|
|
required?: boolean;
|
|
sortOrder?: number;
|
|
}
|
|
|
|
export interface DictionaryStatusDto {
|
|
status?: string;
|
|
operatorId?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
export interface DictionaryListQuery {
|
|
tenantId?: string;
|
|
applicationId?: string;
|
|
keyword?: string;
|
|
status?: string;
|
|
enterpriseKeyword?: string;
|
|
applicationKeyword?: string;
|
|
phoneNumber?: string;
|
|
reasonKeyword?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class DictionariesService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
@Optional() private readonly phoneRoutingLookup?: PhoneRoutingLookupService,
|
|
) {}
|
|
|
|
async listAdministrativeRegions() {
|
|
const rows = await this.prisma.phoneSegment.findMany({
|
|
where: { province: { not: null } },
|
|
select: { province: true, city: true },
|
|
distinct: ['province', 'city'],
|
|
});
|
|
const citiesByProvince = new Map<string, Set<string>>();
|
|
for (const row of rows) {
|
|
const province = row.province?.trim();
|
|
if (!province) continue;
|
|
const cities = citiesByProvince.get(province) ?? new Set<string>();
|
|
const city = row.city?.trim();
|
|
if (city) cities.add(city);
|
|
citiesByProvince.set(province, cities);
|
|
}
|
|
return Array.from(citiesByProvince, ([province, cities]) => ({
|
|
province,
|
|
cities: Array.from(cities).sort((left, right) => left.localeCompare(right, 'zh-CN')),
|
|
})).sort((left, right) => left.province.localeCompare(right.province, 'zh-CN'));
|
|
}
|
|
|
|
async listPhoneSegments(query: PhoneSegmentListQuery = {}) {
|
|
const page = Math.max(1, Number(query.page ?? 1));
|
|
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize ?? 25)));
|
|
const keyword = query.keyword?.trim();
|
|
const where = keyword ? {
|
|
OR: [
|
|
{ prefix: { startsWith: keyword } },
|
|
{ carrier: { contains: keyword } },
|
|
{ province: { contains: keyword } },
|
|
{ city: { contains: keyword } },
|
|
],
|
|
} : undefined;
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.phoneSegment.findMany({ where, orderBy: { prefix: 'asc' }, skip: (page - 1) * pageSize, take: pageSize }),
|
|
this.prisma.phoneSegment.count({ where }),
|
|
]);
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
createPhoneSegment(data: CreatePhoneSegmentDto) {
|
|
return this.prisma.phoneSegment.create({ data });
|
|
}
|
|
|
|
deletePhoneSegment(id: string) {
|
|
return this.prisma.phoneSegment.delete({ where: { id } });
|
|
}
|
|
|
|
async listPhoneCarrierRules(query: PageQuery = {}) {
|
|
const page = Math.max(1, Number(query.page ?? 1));
|
|
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize ?? 25)));
|
|
const where = query.keyword?.trim() ? {
|
|
OR: [
|
|
{ carrier: { contains: query.keyword.trim() } },
|
|
{ pattern: { contains: query.keyword.trim() } },
|
|
{ remark: { contains: query.keyword.trim() } },
|
|
],
|
|
} : undefined;
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.phoneCarrierRule.findMany({ where, orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }], skip: (page - 1) * pageSize, take: pageSize }),
|
|
this.prisma.phoneCarrierRule.count({ where }),
|
|
]);
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
async createPhoneCarrierRule(data: CreatePhoneCarrierRuleDto) {
|
|
if (!data.carrier || !data.pattern) {
|
|
throw new BadRequestException('carrier and pattern are required');
|
|
}
|
|
try {
|
|
new RegExp(data.pattern);
|
|
} catch {
|
|
throw new BadRequestException('pattern must be a valid regular expression');
|
|
}
|
|
const created = await this.prisma.phoneCarrierRule.create({
|
|
data: {
|
|
carrier: data.carrier,
|
|
pattern: data.pattern,
|
|
priority: data.priority ?? 100,
|
|
status: data.status ?? 'active',
|
|
remark: data.remark,
|
|
},
|
|
});
|
|
this.phoneRoutingLookup?.invalidateCarrierRules();
|
|
return created;
|
|
}
|
|
|
|
async deletePhoneCarrierRule(id: string) {
|
|
const deleted = await this.prisma.phoneCarrierRule.delete({ where: { id } });
|
|
this.phoneRoutingLookup?.invalidateCarrierRules();
|
|
return deleted;
|
|
}
|
|
|
|
listSensitiveWords(query: DictionaryListQuery = {}) {
|
|
return this.prisma.sensitiveWord.findMany({
|
|
where: {
|
|
status: visibleDictionaryStatus(query.status),
|
|
OR: query.keyword ? [
|
|
{ word: { contains: query.keyword } },
|
|
{ level: { contains: query.keyword } },
|
|
] : undefined,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createSensitiveWord(data: CreateSensitiveWordDto) {
|
|
const created = await this.prisma.sensitiveWord.create({
|
|
data: {
|
|
word: data.word,
|
|
level: data.level ?? 'block',
|
|
status: data.status ?? 'active',
|
|
},
|
|
});
|
|
await this.writeOperationLog(undefined, 'sensitive_word.create', 'sensitive_word', created.id, { word: data.word });
|
|
return created;
|
|
}
|
|
|
|
async changeSensitiveWordStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status ?? 'active';
|
|
const updated = await this.prisma.sensitiveWord.update({ where: { id }, data: { status } });
|
|
await this.writeOperationLog(data.operatorId, `sensitive_word.${status}`, 'sensitive_word', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
listGlobalBlacklist(query: DictionaryListQuery = {}) {
|
|
return this.prisma.globalBlacklist.findMany({
|
|
where: {
|
|
status: visibleDictionaryStatus(query.status),
|
|
OR: query.keyword ? [
|
|
{ phoneNumber: { contains: query.keyword } },
|
|
{ reason: { contains: query.keyword } },
|
|
] : undefined,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createGlobalBlacklist(data: CreateBlacklistDto) {
|
|
const created = await this.createBlacklistOrConflict(() => this.prisma.globalBlacklist.create({
|
|
data: {
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
status: data.status ?? 'active',
|
|
},
|
|
}), 'global');
|
|
await this.writeOperationLog(data.operatorId, 'global_blacklist.create', 'global_blacklist', created.id, {
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
});
|
|
return created;
|
|
}
|
|
|
|
async changeGlobalBlacklistStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status ?? 'active';
|
|
const updated = await this.prisma.globalBlacklist.update({ where: { id }, data: { status } });
|
|
await this.writeOperationLog(data.operatorId, `global_blacklist.${status}`, 'global_blacklist', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
listEnterpriseBlacklist(query: DictionaryListQuery = {}) {
|
|
return this.prisma.enterpriseBlacklist.findMany({
|
|
where: {
|
|
tenantId: query.tenantId,
|
|
applicationId: query.applicationId,
|
|
status: visibleDictionaryStatus(query.status),
|
|
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
|
|
application: query.applicationKeyword ? { name: { contains: query.applicationKeyword } } : undefined,
|
|
phoneNumber: query.phoneNumber ? { contains: query.phoneNumber } : undefined,
|
|
reason: query.reasonKeyword ? { contains: query.reasonKeyword } : 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, application: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async createEnterpriseBlacklist(data: CreateBlacklistDto) {
|
|
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',
|
|
};
|
|
const created = await this.createBlacklistOrConflict(() => this.prisma.enterpriseBlacklist.create({ data: createData }), 'enterprise');
|
|
await this.writeOperationLog(data.operatorId, 'enterprise_blacklist.create', 'enterprise_blacklist', created.id, {
|
|
tenantId: data.tenantId,
|
|
applicationId: data.applicationId,
|
|
phoneNumber: data.phoneNumber,
|
|
reason: data.reason,
|
|
});
|
|
return created;
|
|
}
|
|
|
|
async changeEnterpriseBlacklistStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status ?? 'active';
|
|
const updated = await this.prisma.enterpriseBlacklist.update({ where: { id }, data: { status } });
|
|
await this.writeOperationLog(data.operatorId, `enterprise_blacklist.${status}`, 'enterprise_blacklist', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
private async createBlacklistOrConflict<T>(operation: () => Promise<T>, scope: 'global' | 'enterprise') {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
if ((error as { code?: string }).code !== 'P2002') throw error;
|
|
throw new ConflictException({
|
|
code: 'BLACKLIST_DUPLICATE',
|
|
field: 'phoneNumber',
|
|
message: scope === 'global'
|
|
? '该手机号已存在于全局黑名单;停用或逻辑删除后请恢复原记录'
|
|
: '该手机号已存在于当前应用黑名单;停用或逻辑删除后请恢复原记录',
|
|
});
|
|
}
|
|
}
|
|
|
|
async listDrainageFields() {
|
|
const fields = await this.prisma.drainageField.findMany({
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
const fieldIds = fields.map((field) => field.id);
|
|
const [channelReferences, commonReferences] = fieldIds.length ? await Promise.all([
|
|
this.prisma.channelReportField.findMany({
|
|
where: {
|
|
drainageFieldId: { in: fieldIds },
|
|
channel: { status: { not: 'deleted' } },
|
|
},
|
|
select: { drainageFieldId: true, channelId: true },
|
|
}),
|
|
this.prisma.commonReportField.findMany({
|
|
where: { drainageFieldId: { in: fieldIds } },
|
|
select: { drainageFieldId: true },
|
|
}),
|
|
]) : [[], []];
|
|
const channelsByField = new Map<string, Set<string>>();
|
|
for (const reference of channelReferences) {
|
|
if (!reference.drainageFieldId) continue;
|
|
const channelIds = channelsByField.get(reference.drainageFieldId) ?? new Set<string>();
|
|
channelIds.add(reference.channelId);
|
|
channelsByField.set(reference.drainageFieldId, channelIds);
|
|
}
|
|
const commonCountByField = new Map<string, number>();
|
|
for (const reference of commonReferences) {
|
|
commonCountByField.set(reference.drainageFieldId, (commonCountByField.get(reference.drainageFieldId) ?? 0) + 1);
|
|
}
|
|
return fields.map((field) => ({
|
|
...field,
|
|
usageCount: channelsByField.get(field.id)?.size ?? 0,
|
|
commonUsageCount: commonCountByField.get(field.id) ?? 0,
|
|
}));
|
|
}
|
|
|
|
createDrainageField(data: CreateDrainageFieldDto) {
|
|
const code = data.code?.trim();
|
|
if (!code || !/^[A-Za-z0-9]+$/.test(code)) {
|
|
throw new BadRequestException('code must contain only Arabic numerals and English letters');
|
|
}
|
|
if (!['string', 'image', 'file'].includes(data.fieldType)) {
|
|
throw new BadRequestException('fieldType must be string, image or file');
|
|
}
|
|
return this.prisma.drainageField.create({
|
|
data: {
|
|
code,
|
|
name: data.name,
|
|
fieldType: data.fieldType,
|
|
required: data.required ?? false,
|
|
status: data.status ?? 'active',
|
|
description: data.description,
|
|
},
|
|
});
|
|
}
|
|
|
|
async deleteDrainageField(id: string) {
|
|
const [usageCount, commonUsageCount] = await Promise.all([
|
|
this.prisma.channelReportField.count({
|
|
where: { drainageFieldId: id, channel: { status: { not: 'deleted' } } },
|
|
}),
|
|
this.prisma.commonReportField.count({ where: { drainageFieldId: id } }),
|
|
]);
|
|
if (usageCount > 0 || commonUsageCount > 0) {
|
|
throw new BadRequestException(`该字段已被 ${usageCount} 个通道和 ${commonUsageCount} 个通用配置使用,不能删除`);
|
|
}
|
|
return this.prisma.$transaction(async (tx) => {
|
|
await tx.channelReportField.deleteMany({
|
|
where: { drainageFieldId: id, channel: { status: 'deleted' } },
|
|
});
|
|
return tx.drainageField.delete({ where: { id } });
|
|
});
|
|
}
|
|
|
|
listDrainageDetectionRules(query: { keyword?: string; status?: string } = {}) {
|
|
const keyword = query.keyword?.trim();
|
|
return this.prisma.drainageDetectionRule.findMany({
|
|
where: {
|
|
status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
|
|
OR: keyword ? [
|
|
{ code: { contains: keyword, mode: 'insensitive' } },
|
|
{ name: { contains: keyword, mode: 'insensitive' } },
|
|
{ description: { contains: keyword, mode: 'insensitive' } },
|
|
] : undefined,
|
|
},
|
|
orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
|
|
});
|
|
}
|
|
|
|
async createDrainageDetectionRule(data: UpsertDrainageDetectionRuleDto) {
|
|
this.validateDrainageDetectionRule(data);
|
|
const created = await this.prisma.drainageDetectionRule.create({
|
|
data: {
|
|
code: data.code.trim().toUpperCase(),
|
|
name: data.name.trim(),
|
|
category: data.category,
|
|
pattern: data.pattern,
|
|
flags: data.flags ?? 'giu',
|
|
priority: data.priority ?? 100,
|
|
status: data.status ?? 'active',
|
|
description: data.description?.trim() || null,
|
|
},
|
|
});
|
|
invalidateDrainageDetectionRuleCache();
|
|
await this.writeOperationLog(data.operatorId, 'drainage_detection_rule.create', 'drainage_detection_rule', created.id, { code: created.code });
|
|
return created;
|
|
}
|
|
|
|
async updateDrainageDetectionRule(id: string, data: UpsertDrainageDetectionRuleDto) {
|
|
this.validateDrainageDetectionRule(data);
|
|
const updated = await this.prisma.drainageDetectionRule.update({
|
|
where: { id },
|
|
data: {
|
|
code: data.code.trim().toUpperCase(),
|
|
name: data.name.trim(),
|
|
category: data.category,
|
|
pattern: data.pattern,
|
|
flags: data.flags ?? 'giu',
|
|
priority: data.priority ?? 100,
|
|
status: data.status ?? 'active',
|
|
description: data.description?.trim() || null,
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
invalidateDrainageDetectionRuleCache();
|
|
await this.writeOperationLog(data.operatorId, 'drainage_detection_rule.update', 'drainage_detection_rule', id, { code: updated.code, version: updated.version });
|
|
return updated;
|
|
}
|
|
|
|
async changeDrainageDetectionRuleStatus(id: string, data: DictionaryStatusDto) {
|
|
const status = data.status === 'inactive' ? 'inactive' : 'active';
|
|
const updated = await this.prisma.drainageDetectionRule.update({
|
|
where: { id },
|
|
data: { status, version: { increment: 1 } },
|
|
});
|
|
invalidateDrainageDetectionRuleCache();
|
|
await this.writeOperationLog(data.operatorId, `drainage_detection_rule.${status}`, 'drainage_detection_rule', id, { reason: data.reason });
|
|
return updated;
|
|
}
|
|
|
|
async testDrainageDetection(data: TestDrainageDetectionDto) {
|
|
if (!data.content?.trim()) throw new BadRequestException('测试短信内容不能为空');
|
|
const rules = data.rule
|
|
? [{
|
|
id: 'preview',
|
|
code: data.rule.code?.trim().toUpperCase() || 'PREVIEW',
|
|
name: data.rule.name?.trim() || '预览规则',
|
|
category: data.rule.category,
|
|
pattern: data.rule.pattern,
|
|
flags: data.rule.flags ?? 'giu',
|
|
priority: data.rule.priority ?? 100,
|
|
version: 1,
|
|
}]
|
|
: await this.prisma.drainageDetectionRule.findMany({ where: { status: 'active' }, orderBy: { priority: 'asc' } });
|
|
if (data.rule) this.validateDrainageDetectionRule(data.rule);
|
|
return detectDrainageContentWithRules(data.content, rules);
|
|
}
|
|
|
|
private validateDrainageDetectionRule(data: UpsertDrainageDetectionRuleDto) {
|
|
if (!data.code?.trim() || !data.name?.trim()) throw new BadRequestException('规则编码和名称不能为空');
|
|
if (!['url', 'mobile', 'landline'].includes(data.category)) throw new BadRequestException('规则类型仅支持 URL、手机号或固话');
|
|
if (data.status && !['active', 'inactive'].includes(data.status)) throw new BadRequestException('规则状态不正确');
|
|
validateDrainageDetectionPattern(data.pattern, data.flags ?? 'giu');
|
|
}
|
|
|
|
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 } });
|
|
}
|
|
|
|
async updateCommonReportField(id: string, data: CreateCommonReportFieldDto, operatorId?: string) {
|
|
if (!['signature', 'drainage'].includes(data.reportType) || typeof data.required !== 'boolean') {
|
|
throw new BadRequestException('资料用途或是否必填无效');
|
|
}
|
|
if (data.sortOrder !== undefined && !Number.isInteger(data.sortOrder)) {
|
|
throw new BadRequestException('排序值必须为整数');
|
|
}
|
|
const existing = await this.prisma.commonReportField.findUnique({ where: { id } });
|
|
if (!existing) throw new NotFoundException('通用字段配置不存在');
|
|
const field = await this.prisma.drainageField.findUnique({ where: { id: data.drainageFieldId } });
|
|
if (!field || field.status !== 'active') throw new BadRequestException('报备字段库字段不存在或已停用');
|
|
const duplicate = await this.prisma.commonReportField.findUnique({
|
|
where: { drainageFieldId_reportType: { drainageFieldId: field.id, reportType: data.reportType } },
|
|
});
|
|
if (duplicate && duplicate.id !== id) throw new ConflictException('该字段已配置为对应类型的通用字段');
|
|
try {
|
|
return await this.prisma.$transaction(async (tx) => {
|
|
const updated = await tx.commonReportField.update({
|
|
where: { id },
|
|
data: { drainageFieldId: field.id, reportType: data.reportType, required: data.required, sortOrder: data.sortOrder },
|
|
include: { drainageField: true },
|
|
});
|
|
await tx.operationLog.create({ data: {
|
|
userId: operatorId, action: 'common_report_field.update', resource: 'common_report_field', resourceId: id,
|
|
detail: { before: { drainageFieldId: existing.drainageFieldId, reportType: existing.reportType, required: existing.required }, after: { drainageFieldId: field.id, reportType: data.reportType, required: data.required } },
|
|
} });
|
|
return updated;
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
|
throw new ConflictException('该字段已配置为对应类型的通用字段');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private writeOperationLog(userId: string | undefined, action: string, resource: string, resourceId: string, detail: Record<string, unknown>) {
|
|
return this.prisma.operationLog.create({
|
|
data: {
|
|
userId,
|
|
action,
|
|
resource,
|
|
resourceId,
|
|
detail: detail as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
}
|
|
}
|