feat: complete drainage review and admin search workflows

This commit is contained in:
hectorzhao
2026-07-14 09:58:18 +08:00
parent dec2430b7e
commit 6421671259
33 changed files with 1107 additions and 255 deletions
+296 -77
View File
@@ -52,6 +52,22 @@ export type UpdateSmsSignatureDto = Partial<Omit<CreateSmsSignatureDto, 'tenantI
auditStatus?: string;
};
export interface CreateSmsDrainageInfoDto {
siteName: string;
url: string;
remark?: string;
reportValues?: Record<string, unknown>;
}
export type UpdateSmsDrainageInfoDto = Partial<CreateSmsDrainageInfoDto>;
export interface DrainageInfoListQuery {
tenantId?: string;
signatureId?: string;
status?: string;
keyword?: string;
}
export interface CreateSignatureMaterialDto {
signatureId: string;
fileObjectId?: string;
@@ -93,14 +109,31 @@ export interface TemplateListQuery {
tenantId?: string;
status?: string;
keyword?: string;
enterpriseKeyword?: string;
applicationKeyword?: string;
nameKeyword?: string;
contentKeyword?: string;
}
export interface ApplicationListQuery {
tenantId?: string;
keyword?: string;
enterpriseKeyword?: string;
applicationKeyword?: string;
status?: string;
includeConnections?: boolean;
}
export interface SignatureListQuery {
tenantId?: string;
keyword?: string;
status?: string;
enterpriseKeyword?: string;
applicationKeyword?: string;
signatureKeyword?: string;
drainageKeyword?: string;
}
export interface GatewayDownstreamConnectionEventDto {
account: string;
connectionId: string;
@@ -130,6 +163,9 @@ export class SmsConfigService {
const applications = await this.prisma.smsApplication.findMany({
where: {
tenantId: query.tenantId,
status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
name: query.applicationKeyword ? { contains: query.applicationKeyword } : undefined,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ tenant: { name: { contains: query.keyword } } },
@@ -171,7 +207,7 @@ export class SmsConfigService {
});
}
async getApplication(applicationId: string) {
async getApplication(applicationId: string, tenantId?: string) {
const application = await this.prisma.smsApplication.findUnique({
where: { id: applicationId },
include: {
@@ -179,7 +215,7 @@ export class SmsConfigService {
ipAllowlist: true,
},
});
if (!application) {
if (!application || (tenantId && application.tenantId !== tenantId)) {
throw new NotFoundException('Application not found');
}
return application;
@@ -574,12 +610,25 @@ export class SmsConfigService {
});
}
async listSignatures(queryOrTenantId?: string | { tenantId?: string; keyword?: string; status?: string }) {
async listSignatures(queryOrTenantId?: string | SignatureListQuery) {
const query = typeof queryOrTenantId === 'string' ? { tenantId: queryOrTenantId } : queryOrTenantId ?? {};
const signatures = await this.prisma.smsSignature.findMany({
where: {
tenantId: query.tenantId,
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
application: query.applicationKeyword ? { name: { contains: query.applicationKeyword } } : undefined,
name: query.signatureKeyword ? { contains: query.signatureKeyword } : undefined,
drainageItems: query.drainageKeyword ? {
some: {
auditStatus: { not: 'deleted' },
OR: [
{ siteName: { contains: query.drainageKeyword } },
{ url: { contains: query.drainageKeyword } },
{ remark: { contains: query.drainageKeyword } },
],
},
} : undefined,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ purpose: { contains: query.keyword } },
@@ -587,7 +636,13 @@ export class SmsConfigService {
{ application: { name: { contains: query.keyword } } },
] : undefined,
},
include: { materials: true, tenant: true, application: true, reportTasks: { include: { channel: true } } },
include: {
materials: true,
tenant: true,
application: true,
drainageItems: { where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } },
reportTasks: { include: { channel: true, drainageInfo: true } },
},
orderBy: { createdAt: 'desc' },
});
const applicationIds = signatures.map((signature) => signature.applicationId).filter((id): id is string => Boolean(id));
@@ -595,24 +650,43 @@ export class SmsConfigService {
where: { applicationId: { in: applicationIds }, status: 'active' },
include: { group: { include: { items: { include: { channel: { include: { reportFields: true } } } } } } },
}) : [];
return signatures.map((signature) => ({
return signatures.map((signature) => {
const legacyPayload = isRecord(signature.drainageInfo) ? signature.drainageInfo : {};
const drainageLinks = signature.drainageItems.map((item) => ({
id: item.id,
siteName: item.siteName,
url: item.url,
remark: item.remark ?? '',
reportValues: isRecord(item.reportValues) ? item.reportValues : {},
auditStatus: item.auditStatus,
rejectReason: item.rejectReason,
submittedAt: item.submittedAt.toISOString(),
reviewedAt: item.reviewedAt?.toISOString(),
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
}));
return {
...signature,
drainageInfo: { ...legacyPayload, links: drainageLinks },
reportTargets: (() => {
const channels = routes.filter((route) => route.applicationId === signature.applicationId && route.group).flatMap((route) => route.group!.items.map((item) => item.channel)).filter((channel) => channel.status !== 'deleted');
const taskByChannel = new Map((signature.reportTasks ?? []).filter((task) => task.reportType === 'signature').map((task) => [task.channelId, task]));
return [...new Map(channels.map((channel) => [channel.id, channel])).values()].map((channel) => ({ channel, channelId: channel.id, status: taskByChannel.get(channel.id)?.status ?? 'pending', taskId: taskByChannel.get(channel.id)?.id }));
})(),
drainageReportTargets: Object.fromEntries((Array.isArray((signature.drainageInfo as Record<string, unknown> | null)?.links) ? (signature.drainageInfo as Record<string, unknown>).links as Array<Record<string, unknown>> : []).map((link) => {
const drainageItemId = String(link.id ?? '');
drainageReportTargets: Object.fromEntries(signature.drainageItems.map((drainageItem) => {
const drainageItemId = drainageItem.id;
const channels = routes
.filter((route) => route.applicationId === signature.applicationId && route.group)
.flatMap((route) => route.group!.items.map((item) => item.channel))
.filter((channel) => channel.status !== 'deleted' && channel.reportFields.some((field) => field.status === 'active' && ['drainage', 'both'].includes(field.reportType)));
const taskByChannel = new Map((signature.reportTasks ?? []).filter((task) => task.reportType === 'drainage' && task.drainageItemId === drainageItemId).map((task) => [task.channelId, task]));
return [drainageItemId, [...new Map(channels.map((channel) => [channel.id, channel])).values()].map((channel) => ({ channel, channelId: channel.id, status: taskByChannel.get(channel.id)?.status ?? 'pending', taskId: taskByChannel.get(channel.id)?.id }))];
return [drainageItemId, [...new Map(channels.map((channel) => [channel.id, channel])).values()].flatMap((channel) => {
const task = taskByChannel.get(channel.id);
return task ? [{ channel, channelId: channel.id, status: task.status, taskId: task.id }] : [];
})];
})),
drainageCarrierReportSummary: Object.fromEntries((Array.isArray((signature.drainageInfo as Record<string, unknown> | null)?.links) ? (signature.drainageInfo as Record<string, unknown>).links as Array<Record<string, unknown>> : []).map((link) => {
const drainageItemId = String(link.id ?? '');
drainageCarrierReportSummary: Object.fromEntries(signature.drainageItems.map((drainageItem) => {
const drainageItemId = drainageItem.id;
const channels = routes
.filter((route) => route.applicationId === signature.applicationId && route.group)
.flatMap((route) => route.group!.items.map((item) => item.channel))
@@ -621,7 +695,7 @@ export class SmsConfigService {
const taskByChannel = new Map((signature.reportTasks ?? []).filter((task) => task.reportType === 'drainage' && task.drainageItemId === drainageItemId).map((task) => [task.channelId, task]));
return [drainageItemId, Object.fromEntries(['mobile', 'unicom', 'telecom'].map((carrier) => {
const carrierTargets = targets.filter((channel) => channel.carrier === carrier || channel.carrier === 'all');
const statuses = carrierTargets.map((channel) => taskByChannel.get(channel.id)?.status ?? 'pending');
const statuses = carrierTargets.flatMap((channel) => taskByChannel.get(channel.id)?.status ? [taskByChannel.get(channel.id)!.status] : []);
const approved = statuses.filter((status) => status === 'approved').length;
const status = !statuses.length ? 'not_applicable' : approved === statuses.length ? 'approved' : statuses.some((item) => ['failed', 'rejected'].includes(item)) ? 'failed' : statuses.some((item) => ['reporting', 'exporting'].includes(item)) || approved ? 'reporting' : statuses.some((item) => item === 'waiting_material') ? 'waiting_material' : 'pending';
return [carrier, { status, approved, total: statuses.length }];
@@ -636,7 +710,8 @@ export class SmsConfigService {
const status = !targets.length ? 'not_applicable' : approved === targets.length ? 'approved' : statuses.some((item) => ['failed', 'rejected'].includes(item)) ? 'failed' : statuses.some((item) => ['reporting', 'exporting'].includes(item)) || approved ? 'reporting' : statuses.some((item) => item === 'waiting_material') ? 'waiting_material' : 'pending';
return [carrier, { status, approved, total: targets.length }];
})),
}));
};
});
}
async createSignature(data: CreateSmsSignatureDto, options: CreateSmsSignatureOptions = {}) {
@@ -691,6 +766,118 @@ export class SmsConfigService {
return updated;
}
listDrainageInfos(query: DrainageInfoListQuery = {}) {
return this.prisma.smsDrainageInfo.findMany({
where: {
tenantId: query.tenantId,
signatureId: query.signatureId,
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
OR: query.keyword ? [
{ siteName: { contains: query.keyword } },
{ url: { contains: query.keyword } },
{ signature: { name: { contains: query.keyword } } },
{ tenant: { name: { contains: query.keyword } } },
{ application: { name: { contains: query.keyword } } },
] : undefined,
},
include: { tenant: true, signature: true, application: true, reportTasks: { include: { channel: true } } },
orderBy: { updatedAt: 'desc' },
});
}
async createDrainageInfo(signatureId: string, data: CreateSmsDrainageInfoDto, options: CreateSmsSignatureOptions = {}, tenantId?: string) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature) throw new NotFoundException('Signature not found');
if (tenantId && signature.tenantId !== tenantId) throw new NotFoundException('Signature not found');
if (signature.auditStatus !== 'approved') throw new BadRequestException('签名审核通过后才能新增引流信息');
if (!data.siteName?.trim() || !data.url?.trim()) throw new BadRequestException('siteName and url are required');
await this.validateDrainageReportValues(signature.applicationId ?? undefined, data.reportValues);
const auditStatus = options.initialAuditStatus ?? 'pending';
const item = await this.prisma.smsDrainageInfo.create({
data: {
tenantId: signature.tenantId,
signatureId,
applicationId: signature.applicationId,
siteName: data.siteName.trim(),
url: data.url.trim(),
remark: data.remark,
reportValues: data.reportValues as Prisma.InputJsonValue | undefined,
auditStatus,
reviewedAt: auditStatus === 'approved' ? new Date() : undefined,
},
include: { tenant: true, signature: true, application: true },
});
await this.createAuditRecord({
tenantId: item.tenantId,
targetType: 'sms_drainage_info',
targetId: item.id,
action: auditStatus === 'approved' ? 'admin_create_approved' : 'submit',
statusAfter: auditStatus,
reason: auditStatus === 'approved' ? '运营端新建引流信息自动审核通过' : undefined,
});
if (auditStatus === 'approved') await this.activateDrainageReporting(item.id);
return item;
}
async updateDrainageInfo(itemId: string, data: UpdateSmsDrainageInfoDto, options: CreateSmsSignatureOptions = {}, tenantId?: string) {
const current = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId }, include: { signature: true } });
if (!current) throw new NotFoundException('Drainage info not found');
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
if (current.auditStatus === 'deleted') throw new BadRequestException('已删除的引流信息不能修改');
if (data.siteName !== undefined && !data.siteName.trim()) throw new BadRequestException('siteName is required');
if (data.url !== undefined && !data.url.trim()) throw new BadRequestException('url is required');
const applicationId = current.signature.applicationId ?? current.applicationId ?? undefined;
await this.validateDrainageReportValues(applicationId, data.reportValues ?? (isRecord(current.reportValues) ? current.reportValues : {}));
const auditStatus = options.initialAuditStatus ?? 'pending';
const updated = await this.prisma.smsDrainageInfo.update({
where: { id: itemId },
data: {
applicationId,
siteName: data.siteName?.trim(),
url: data.url?.trim(),
remark: data.remark,
reportValues: data.reportValues as Prisma.InputJsonValue | undefined,
auditStatus,
rejectReason: null,
submittedAt: new Date(),
reviewedAt: auditStatus === 'approved' ? new Date() : null,
},
include: { tenant: true, signature: true, application: true },
});
await this.createAuditRecord({
tenantId: current.tenantId,
targetType: 'sms_drainage_info',
targetId: itemId,
action: auditStatus === 'approved' ? 'admin_update_approved' : 'update_submit',
statusBefore: current.auditStatus,
statusAfter: auditStatus,
reason: auditStatus === 'approved' ? '运营端修改引流信息并自动审核通过' : undefined,
});
if (auditStatus === 'approved') await this.activateDrainageReporting(itemId);
else await this.suspendDrainageReporting(itemId, '引流信息修改后等待运营审核');
return updated;
}
approveDrainageInfo(itemId: string, data: ReviewDto) {
return this.reviewDrainageInfo(itemId, 'approved', 'approve', data);
}
rejectDrainageInfo(itemId: string, data: ReviewDto) {
return this.reviewDrainageInfo(itemId, 'rejected', 'reject', data);
}
async changeDrainageInfoStatus(itemId: string, data: StatusChangeDto, tenantId?: string) {
const current = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId } });
if (!current) throw new NotFoundException('Drainage info not found');
if (tenantId && current.tenantId !== tenantId) throw new NotFoundException('Drainage info not found');
const status = data.status ?? 'deleted';
if (tenantId && status !== 'deleted') throw new BadRequestException('客户端只能删除引流信息,不能直接修改审核状态');
const updated = await this.prisma.smsDrainageInfo.update({ where: { id: itemId }, data: { auditStatus: status } });
if (status === 'deleted') await this.suspendDrainageReporting(itemId, data.reason ?? '引流信息已删除', 'abandoned');
await this.createAuditRecord({ tenantId: current.tenantId, targetType: 'sms_drainage_info', targetId: itemId, action: status, statusBefore: current.auditStatus, statusAfter: status, reason: data.reason });
return updated;
}
private async withReportRequirementSnapshot(applicationId?: string, drainageInfo?: Record<string, unknown>) {
if (!drainageInfo || !applicationId) return drainageInfo;
const fields = await this.getApplicationReportFields(applicationId);
@@ -716,21 +903,6 @@ export class SmsConfigService {
if (!applicationId || !drainageInfo) return;
const fields = await this.getApplicationReportFields(applicationId);
const signatureValues = isRecord(drainageInfo.signatureReportValues) ? drainageInfo.signatureReportValues : {};
const links = Array.isArray(drainageInfo.links) ? drainageInfo.links.filter(isRecord) : [];
const drainageItemIds = links.map((link) => String(link.id ?? '')).filter(Boolean);
await this.prisma.drainageReportMaterial.deleteMany({
where: {
signatureId,
...(drainageItemIds.length > 0 ? { drainageItemId: { notIn: drainageItemIds } } : {}),
},
});
await this.prisma.channelSignatureReportTask.deleteMany({
where: {
signatureId,
reportType: 'drainage',
...(drainageItemIds.length > 0 ? { drainageItemId: { notIn: drainageItemIds } } : {}),
},
});
for (const field of fields.filter((item) => item.reportTypes.some((type) => type === 'signature' || type === 'both'))) {
const value = reportValueParts(signatureValues[field.code]);
for (const channel of field.channels) {
@@ -741,46 +913,6 @@ export class SmsConfigService {
});
}
}
const drainageFields = fields.filter((item) => item.reportTypes.some((type) => type === 'drainage' || type === 'both'));
const drainageChannels = new Map(drainageFields.flatMap((item) => item.channels).map((channel) => [channel.id, channel]));
const signatureOwner = links.length > 0
? await this.prisma.smsSignature.findUnique({ where: { id: signatureId }, select: { tenantId: true } })
: null;
for (const link of links) {
const drainageItemId = String(link.id ?? '');
const values = isRecord(link.reportValues) ? link.reportValues : {};
if (!drainageItemId) continue;
for (const channel of drainageChannels.values()) {
const existingTask = await this.prisma.channelSignatureReportTask.findFirst({
where: { signatureId, channelId: channel.id, reportType: 'drainage', drainageItemId },
});
if (!existingTask && signatureOwner) {
const task = await this.prisma.channelSignatureReportTask.create({
data: { tenantId: signatureOwner.tenantId, signatureId, channelId: channel.id, reportType: 'drainage', drainageItemId, status: 'pending' },
});
await this.prisma.channelSignatureReportRecord.create({
data: { taskId: task.id, channelId: channel.id, action: 'create', statusAfter: 'pending' },
});
}
}
for (const field of drainageFields) {
const value = reportValueParts(values[field.code]);
for (const channel of field.channels) {
await this.prisma.drainageReportMaterial.upsert({
where: {
signatureId_drainageItemId_channelId_fieldCode: {
signatureId,
drainageItemId,
channelId: channel.id,
fieldCode: field.code,
},
},
update: value,
create: { signatureId, drainageItemId, channelId: channel.id, fieldCode: field.code, ...value },
});
}
}
}
}
private async validateSignatureReportValues(applicationId?: string, drainageInfo?: Record<string, unknown>) {
@@ -793,19 +925,68 @@ export class SmsConfigService {
if (missingSignature.length > 0) {
throw new BadRequestException(`缺少必填签名报备资料:${missingSignature.map((field) => field.name).join('、')}`);
}
const drainageFields = fields.filter(
(field) => field.required && field.reportTypes.some((type) => type === 'drainage' || type === 'both'),
);
const links = Array.isArray(drainageInfo.links) ? drainageInfo.links.filter(isRecord) : [];
for (const link of links) {
const values = isRecord(link.reportValues) ? link.reportValues : {};
const missing = drainageFields.filter((field) => !hasReportValue(values[field.code]));
if (missing.length > 0) {
throw new BadRequestException(`引流信息缺少必填报备资料:${missing.map((field) => field.name).join('、')}`);
}
}
private async validateDrainageReportValues(applicationId?: string, reportValues: Record<string, unknown> = {}) {
if (!applicationId) return;
const fields = await this.getApplicationReportFields(applicationId, 'drainage');
const missing = fields.filter((field) => field.required && !hasReportValue(reportValues[field.code]));
if (missing.length > 0) {
throw new BadRequestException(`引流信息缺少必填报备资料:${missing.map((field) => field.name).join('、')}`);
}
}
private async activateDrainageReporting(itemId: string) {
const item = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId }, include: { signature: true } });
if (!item) throw new NotFoundException('Drainage info not found');
if (item.auditStatus !== 'approved') throw new BadRequestException('引流信息审核通过后才能进入通道报备');
const applicationId = item.signature.applicationId ?? item.applicationId ?? undefined;
if (!applicationId) return;
const fields = (await this.getApplicationReportFields(applicationId, 'drainage'))
.filter((field) => field.reportTypes.some((type) => type === 'drainage' || type === 'both'));
const channels = new Map(fields.flatMap((field) => field.channels).map((channel) => [channel.id, channel]));
const values = isRecord(item.reportValues) ? item.reportValues : {};
await this.prisma.$transaction(async (tx) => {
await tx.drainageReportMaterial.deleteMany({ where: { signatureId: item.signatureId, drainageItemId: item.id } });
for (const field of fields) {
const value = reportValueParts(values[field.code]);
for (const channel of field.channels) {
await tx.drainageReportMaterial.create({
data: { signatureId: item.signatureId, drainageItemId: item.id, channelId: channel.id, fieldCode: field.code, ...value },
});
}
}
const existingTasks = await tx.channelSignatureReportTask.findMany({ where: { drainageItemId: item.id, reportType: 'drainage' } });
const existingByChannel = new Map(existingTasks.map((task) => [task.channelId, task]));
for (const channel of channels.values()) {
const existing = existingByChannel.get(channel.id);
const task = existing
? await tx.channelSignatureReportTask.update({ where: { id: existing.id }, data: { status: 'pending', reason: null } })
: await tx.channelSignatureReportTask.create({ data: { tenantId: item.tenantId, signatureId: item.signatureId, channelId: channel.id, reportType: 'drainage', drainageItemId: item.id, status: 'pending' } });
await tx.channelSignatureReportRecord.create({
data: { taskId: task.id, channelId: channel.id, action: existing ? 'audit_approved_reset' : 'audit_approved_create', statusBefore: existing?.status, statusAfter: 'pending', reason: '引流信息运营审核通过' },
});
}
for (const task of existingTasks.filter((current) => !channels.has(current.channelId) && current.status !== 'abandoned')) {
await tx.channelSignatureReportTask.update({ where: { id: task.id }, data: { status: 'abandoned', reason: '应用当前路由已不包含此通道' } });
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'route_removed', statusBefore: task.status, statusAfter: 'abandoned', reason: '应用当前路由已不包含此通道' } });
}
});
}
private async suspendDrainageReporting(itemId: string, reason: string, statusAfter = 'waiting_review') {
await this.prisma.$transaction(async (tx) => {
const item = await tx.smsDrainageInfo.findUnique({ where: { id: itemId } });
if (!item) throw new NotFoundException('Drainage info not found');
await tx.drainageReportMaterial.deleteMany({ where: { signatureId: item.signatureId, drainageItemId: item.id } });
const tasks = await tx.channelSignatureReportTask.findMany({ where: { drainageItemId: item.id, reportType: 'drainage' } });
for (const task of tasks.filter((current) => current.status !== statusAfter)) {
await tx.channelSignatureReportTask.update({ where: { id: task.id }, data: { status: statusAfter, reason } });
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'audit_suspended', statusBefore: task.status, statusAfter, reason } });
}
});
}
createSignatureMaterial(data: CreateSignatureMaterialDto) {
return this.prisma.signatureMaterial.create({
data: {
@@ -845,6 +1026,10 @@ export class SmsConfigService {
where: {
tenantId: query.tenantId,
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
application: query.applicationKeyword ? { name: { contains: query.applicationKeyword } } : undefined,
name: query.nameKeyword ? { contains: query.nameKeyword } : undefined,
content: query.contentKeyword ? { contains: query.contentKeyword } : undefined,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ content: { contains: query.keyword } },
@@ -1030,6 +1215,40 @@ export class SmsConfigService {
return updated;
}
private async reviewDrainageInfo(itemId: string, statusAfter: string, action: string, data: ReviewDto) {
const item = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId } });
if (!item) throw new NotFoundException('Drainage info not found');
if (!['pending', 'rejected'].includes(item.auditStatus)) {
throw new BadRequestException('只有待审核或已驳回的引流信息可以审核');
}
if (statusAfter === 'rejected' && !data.reason?.trim()) {
throw new BadRequestException('驳回引流信息时必须填写原因');
}
const reviewerId = await this.resolveReviewerId(data.reviewerId);
const updated = await this.prisma.smsDrainageInfo.update({
where: { id: itemId },
data: {
auditStatus: statusAfter,
rejectReason: statusAfter === 'rejected' ? data.reason?.trim() : null,
reviewedAt: new Date(),
},
include: { tenant: true, signature: true, application: true },
});
await this.createAuditRecord({
tenantId: item.tenantId,
targetType: 'sms_drainage_info',
targetId: itemId,
action,
statusBefore: item.auditStatus,
statusAfter,
reason: data.reason,
reviewerId,
});
if (statusAfter === 'approved') await this.activateDrainageReporting(itemId);
else await this.suspendDrainageReporting(itemId, data.reason?.trim() || '引流信息运营审核驳回');
return updated;
}
private async reviewTemplate(templateId: string, statusAfter: string, action: string, data: ReviewDto) {
const template = await this.prisma.smsTemplate.findUnique({ where: { id: templateId } });
if (!template) {