feat: allow deleting notification projects with history

This commit is contained in:
WuYongJie 2026-08-29 21:39:09 +08:00
parent c7fd17df35
commit d74959da1f
2 changed files with 98 additions and 8 deletions

View File

@ -743,7 +743,7 @@ test('admin API 提供稳定 contract,rotate-secret/test/deliveries/audits 可
headers: withAdminAuth(),
})
assert.equal(deleteResponse.status, 200)
assert.deepEqual(await deleteResponse.json(), { projectCode: 'temp_unused', deleted: true })
assert.deepEqual(await deleteResponse.json(), { projectCode: 'temp_unused', deleted: true, deliveryCount: 0 })
const oversizedResponse = await fetch(`http://127.0.0.1:${port}/admin/projects`, {
method: 'POST',
@ -769,3 +769,95 @@ test('admin API 提供稳定 contract,rotate-secret/test/deliveries/audits 可
await prisma.$disconnect()
}
})
test('已有投递记录的项目也可删除,项目配置与 recipients 删除但历史记录保留', async () => {
const oldEncryptionKey = process.env.YYD_NOTIFICATION_CONFIG_ENCRYPTION_KEY
const oldAdminApiSecret = process.env.YYD_NOTIFICATION_ADMIN_API_SECRET
const oldAdminToken = process.env.YYD_NOTIFICATION_ADMIN_TOKEN
const oldProjectsJson = process.env.YYD_NOTIFICATION_PROJECTS_JSON
const oldPublicBaseUrl = process.env.YYD_NOTIFICATION_PUBLIC_BASE_URL
process.env.YYD_NOTIFICATION_CONFIG_ENCRYPTION_KEY = encryptionKey
process.env.YYD_NOTIFICATION_ADMIN_API_SECRET = adminApiSecret
process.env.YYD_NOTIFICATION_ADMIN_TOKEN = ''
process.env.YYD_NOTIFICATION_PROJECTS_JSON = ''
process.env.YYD_NOTIFICATION_PUBLIC_BASE_URL = 'https://sc.example.com/notification-center'
await resetDatabase()
const registry = new AdminProjectRegistry()
await registry.refresh()
const server = createServer(registry, { resolveRecipientAlias }).listen(0)
await new Promise<void>((resolve) => server.once('listening', resolve))
const port = (server.address() as AddressInfo).port
try {
const createResponse = await fetch(`http://127.0.0.1:${port}/admin/projects`, {
method: 'POST',
headers: withAdminAuth(),
body: JSON.stringify({
projectCode: 'delete_with_history',
name: '删除保留历史',
recipients: [{ alias: '海棠 新', userId: 'ding-ops-user-2' }],
}),
})
assert.equal(createResponse.status, 201)
await prisma.notificationDelivery.create({
data: {
id: 'delivery-keep-after-project-delete',
projectCode: 'delete_with_history',
idempotencyKey: 'delete_with_history:1',
requestHash: 'request-hash',
recipientUserId: 'ding-ops-user-2',
businessTitle: '历史任务',
renderedTitle: '【删除保留历史】历史任务',
content: '历史正文',
actionTitle: '查看',
actionUrl: '',
status: 'failed',
lastErrorCode: 'INVALID_USER',
lastErrorMessage: '历史失败保留',
},
})
const deleteResponse = await fetch(`http://127.0.0.1:${port}/admin/projects/delete_with_history`, {
method: 'DELETE',
headers: withAdminAuth(),
})
assert.equal(deleteResponse.status, 200)
assert.deepEqual(await deleteResponse.json(), {
projectCode: 'delete_with_history',
deleted: true,
deliveryCount: 1,
})
assert.equal(await prisma.notificationProject.findUnique({ where: { code: 'delete_with_history' } }), null)
assert.equal(await prisma.notificationProjectRecipient.count({ where: { projectId: { not: '' } } }), 0)
const delivery = await prisma.notificationDelivery.findUniqueOrThrow({
where: { id: 'delivery-keep-after-project-delete' },
})
assert.equal(delivery.projectCode, 'delete_with_history')
assert.equal(delivery.businessTitle, '历史任务')
const audit = await prisma.notificationAudit.findFirstOrThrow({
where: {
action: 'project.delete',
targetProjectCode: 'delete_with_history',
},
orderBy: { createdAt: 'desc' },
})
assert.equal(audit.summary, '删除项目 delete_with_history')
assert.deepEqual(JSON.parse(audit.detailsJson || '{}'), {
recipientCount: 1,
deliveryCount: 1,
})
} finally {
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
process.env.YYD_NOTIFICATION_CONFIG_ENCRYPTION_KEY = oldEncryptionKey
process.env.YYD_NOTIFICATION_ADMIN_API_SECRET = oldAdminApiSecret
process.env.YYD_NOTIFICATION_ADMIN_TOKEN = oldAdminToken
process.env.YYD_NOTIFICATION_PROJECTS_JSON = oldProjectsJson
process.env.YYD_NOTIFICATION_PUBLIC_BASE_URL = oldPublicBaseUrl
await prisma.$disconnect()
}
})

View File

@ -810,11 +810,6 @@ export class AdminProjectRegistry {
async deleteProject(projectCode: string, actor: AdminAuditActor) {
const current = await this.loadExistingProjectForMutation(projectCode)
const deliveryCount = await prisma.notificationDelivery.count({ where: { projectCode } })
if (deliveryCount > 0) {
throw new AdminApiError(409, 'PROJECT_HAS_DELIVERIES', `项目 ${projectCode} 已存在投递记录,请先停用而不是删除`, {
deliveryCount,
})
}
await prisma.$transaction(async (tx) => {
await tx.notificationProject.delete({ where: { code: projectCode } })
await tx.notificationAudit.create({
@ -827,12 +822,15 @@ export class AdminProjectRegistry {
requestId: actor.requestId || null,
ipAddress: actor.ipAddress || null,
summary: `删除项目 ${projectCode}`,
detailsJson: createAuditDetailsJson({ recipientCount: current.recipients.length }),
detailsJson: createAuditDetailsJson({
recipientCount: current.recipients.length,
deliveryCount,
}),
},
})
})
await this.refresh()
return { projectCode, deleted: true }
return { projectCode, deleted: true, deliveryCount }
}
async importFromEnvironment(actor: AdminAuditActor) {