feat: add HTTP API and complete client workflows

This commit is contained in:
hectorzhao
2026-07-16 11:34:06 +08:00
parent 4f07b331e5
commit dcb6162dcf
40 changed files with 2548 additions and 365 deletions
+154 -6
View File
@@ -180,6 +180,7 @@ export class SmsConfigService {
include: {
tenant: true,
ipAllowlist: true,
httpConfig: true,
},
orderBy: { createdAt: 'desc' },
});
@@ -221,6 +222,7 @@ export class SmsConfigService {
include: {
tenant: true,
ipAllowlist: true,
httpConfig: true,
},
});
if (!application || (tenantId && application.tenantId !== tenantId)) {
@@ -350,6 +352,11 @@ export class SmsConfigService {
return Array.from(merged.values());
}
async getClientApplicationReportFields(applicationId?: string, reportType?: 'signature' | 'drainage') {
const fields = await this.getApplicationReportFields(applicationId, reportType);
return fields.map(({ channels: _channels, commonReportTypes: _commonReportTypes, ...field }) => field);
}
async createApplication(data: CreateSmsApplicationDto) {
const secret = normalizeApplicationPassword(data.passwordCipher);
const queuePriority = normalizeApplicationQueuePriority(data.queuePriority);
@@ -808,6 +815,128 @@ export class SmsConfigService {
});
}
async listClientSignatures(tenantId?: string, signatureId?: string) {
const signatures = await this.prisma.smsSignature.findMany({
where: { id: signatureId, tenantId, auditStatus: { notIn: ['deleted', 'disabled'] } },
select: {
id: true,
tenantId: true,
applicationId: true,
name: true,
purpose: true,
auditStatus: true,
rejectReason: true,
drainageInfo: true,
createdAt: true,
updatedAt: true,
application: { select: { id: true, name: true, status: true } },
materials: {
select: { id: true, fileObjectId: true, materialType: true, title: true, description: true, createdAt: true },
},
drainageItems: {
where: { auditStatus: { not: 'deleted' } },
orderBy: { updatedAt: 'desc' },
select: {
id: true,
siteName: true,
url: true,
remark: true,
reportValues: true,
auditStatus: true,
rejectReason: true,
submittedAt: true,
reviewedAt: true,
createdAt: true,
updatedAt: true,
},
},
_count: { select: { reportMaterials: true } },
},
orderBy: { updatedAt: 'desc' },
});
return signatures.map((signature) => {
const stored = isRecord(signature.drainageInfo) ? signature.drainageInfo : {};
return {
id: signature.id,
tenantId: signature.tenantId,
applicationId: signature.applicationId,
name: signature.name,
purpose: signature.purpose,
auditStatus: signature.auditStatus,
rejectReason: signature.rejectReason,
createdAt: signature.createdAt,
updatedAt: signature.updatedAt,
application: signature.application,
materials: signature.materials,
submittedMaterialCount: signature.materials.length + signature._count.reportMaterials,
reportValues: isRecord(stored.signatureReportValues) ? stored.signatureReportValues : {},
drainageInfo: {
links: signature.drainageItems.map((item) => ({
...item,
reportValues: isRecord(item.reportValues) ? item.reportValues : {},
})),
},
};
});
}
async getClientSignatureView(signatureId: string, tenantId?: string) {
const [signature] = await this.listClientSignatures(tenantId, signatureId);
if (!signature) throw new NotFoundException('Signature not found');
return signature;
}
async getClientSignatureWorkspace(tenantId?: string) {
const [items, statusCounts] = await Promise.all([
this.listClientSignatures(tenantId),
this.prisma.smsSignature.groupBy({
by: ['auditStatus'],
where: { tenantId, auditStatus: { notIn: ['deleted', 'disabled'] } },
_count: { _all: true },
}),
]);
const summary = { total: 0, pending: 0, approved: 0, rejected: 0, draft: 0 };
for (const item of statusCounts) {
const count = item._count._all;
summary.total += count;
if (item.auditStatus in summary && item.auditStatus !== 'total') {
summary[item.auditStatus as keyof Omit<typeof summary, 'total'>] = count;
}
}
return { items, summary };
}
async listClientDrainageInfos(tenantId?: string, itemId?: string) {
return this.prisma.smsDrainageInfo.findMany({
where: { id: itemId, tenantId, auditStatus: { not: 'deleted' } },
select: {
id: true,
tenantId: true,
signatureId: true,
applicationId: true,
siteName: true,
url: true,
remark: true,
reportValues: true,
auditStatus: true,
rejectReason: true,
submittedAt: true,
reviewedAt: true,
createdAt: true,
updatedAt: true,
signature: { select: { id: true, name: true, auditStatus: true } },
application: { select: { id: true, name: true, status: true } },
},
orderBy: { updatedAt: 'desc' },
});
}
async getClientDrainageInfoView(itemId: string, tenantId?: string) {
const [item] = await this.listClientDrainageInfos(tenantId, itemId);
if (!item) throw new NotFoundException('Drainage info not found');
return item;
}
async createSignature(data: CreateSmsSignatureDto, options: CreateSmsSignatureOptions = {}) {
await this.validateSignatureReportValues(data.applicationId, data.drainageInfo);
const drainageInfo = await this.withReportRequirementSnapshot(data.applicationId, data.drainageInfo);
@@ -835,9 +964,9 @@ export class SmsConfigService {
return signature;
}
async updateSignature(signatureId: string, data: UpdateSmsSignatureDto) {
async updateSignature(signatureId: string, data: UpdateSmsSignatureDto, tenantId?: string) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature) {
if (!signature || (tenantId && signature.tenantId !== tenantId)) {
throw new NotFoundException('Signature not found');
}
await this.validateSignatureReportValues(data.applicationId ?? signature.applicationId ?? undefined, data.drainageInfo);
@@ -852,6 +981,7 @@ export class SmsConfigService {
name: data.name,
purpose: data.purpose,
auditStatus: data.auditStatus,
rejectReason: data.auditStatus === 'pending' ? null : undefined,
drainageInfo: drainageInfo as Prisma.InputJsonValue | undefined,
materialVersion: { increment: 1 },
pendingReport: true,
@@ -863,6 +993,24 @@ export class SmsConfigService {
return updated;
}
async updateClientSignature(signatureId: string, data: UpdateSmsSignatureDto, tenantId?: string) {
const current = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!current || (tenantId && current.tenantId !== tenantId)) throw new NotFoundException('Signature not found');
if (!['draft', 'rejected', 'approved'].includes(current.auditStatus)) {
throw new BadRequestException('当前审核状态不允许修改签名');
}
const updated = await this.updateSignature(signatureId, { ...data, auditStatus: 'pending' }, tenantId);
await this.createAuditRecord({
tenantId: current.tenantId,
targetType: 'sms_signature',
targetId: signatureId,
action: 'client_update_submit',
statusBefore: current.auditStatus,
statusAfter: 'pending',
});
return updated;
}
listDrainageInfos(query: DrainageInfoListQuery = {}) {
return this.prisma.smsDrainageInfo.findMany({
where: {
@@ -1098,9 +1246,9 @@ export class SmsConfigService {
});
}
async submitSignature(signatureId: string) {
async submitSignature(signatureId: string, tenantId?: string) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature) {
if (!signature || (tenantId && signature.tenantId !== tenantId)) {
throw new NotFoundException('Signature not found');
}
@@ -1285,9 +1433,9 @@ export class SmsConfigService {
return this.reviewTemplate(templateId, 'rejected', 'reject', data);
}
async changeSignatureStatus(signatureId: string, data: StatusChangeDto) {
async changeSignatureStatus(signatureId: string, data: StatusChangeDto, tenantId?: string) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature) {
if (!signature || (tenantId && signature.tenantId !== tenantId)) {
throw new NotFoundException('Signature not found');
}
const status = data.status ?? 'deleted';