From 761c49ed3c753ced8ec2624d13cc6f21f30c1048 Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 24 Jul 2026 06:28:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=86=B2=E7=AA=81=E8=AF=AD=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qqbot-message-subscription.service.ts | 39 +++++++++++-------- ...qqbot-message-subscription.service.spec.ts | 30 ++++++++++++++ 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/modules/qqbot/core/application/message-push/qqbot-message-subscription.service.ts b/src/modules/qqbot/core/application/message-push/qqbot-message-subscription.service.ts index 64ace60..9d93adc 100644 --- a/src/modules/qqbot/core/application/message-push/qqbot-message-subscription.service.ts +++ b/src/modules/qqbot/core/application/message-push/qqbot-message-subscription.service.ts @@ -144,23 +144,28 @@ export class QqbotMessageSubscriptionService { input: MessageSubscriptionInput, ): Promise { const normalized = await this.normalizeInput(input); - const saved = await this.subscriptionRepository.manager.transaction( - /** Holds both target and prospective active-key rows through the update save. */ - async (manager) => { - const repository = manager.getRepository(QqbotMessageSubscription); - const current = await this.findActiveForWrite(repository, id); - const conflict = await repository.findOne({ - lock: { mode: 'pessimistic_write' }, - where: { activeKey: normalized.activeKey, isDeleted: false }, - }); - if (conflict && conflict.id !== current.id) { - this.throwNaturalKeyConflict(); - } - Object.assign(current, normalized); - return repository.save(current); - }, - ); - return this.toView(saved); + try { + const saved = await this.subscriptionRepository.manager.transaction( + /** Holds both target and prospective active-key rows through the update save. */ + async (manager) => { + const repository = manager.getRepository(QqbotMessageSubscription); + const current = await this.findActiveForWrite(repository, id); + const conflict = await repository.findOne({ + lock: { mode: 'pessimistic_write' }, + where: { activeKey: normalized.activeKey, isDeleted: false }, + }); + if (conflict && conflict.id !== current.id) { + this.throwNaturalKeyConflict(); + } + Object.assign(current, normalized); + return repository.save(current); + }, + ); + return this.toView(saved); + } catch (error) { + if (this.isDuplicateKeyError(error)) this.throwNaturalKeyConflict(); + throw error; + } } /** diff --git a/test/modules/qqbot/message-push/qqbot-message-subscription.service.spec.ts b/test/modules/qqbot/message-push/qqbot-message-subscription.service.spec.ts index ca5fa6d..234d623 100644 --- a/test/modules/qqbot/message-push/qqbot-message-subscription.service.spec.ts +++ b/test/modules/qqbot/message-push/qqbot-message-subscription.service.spec.ts @@ -389,6 +389,36 @@ describe('QqbotMessageSubscriptionService', () => { ); }); + it('maps duplicate-key update races to Vben HTTP 409 and propagates other save failures', async () => { + const duplicateKeyRace = setup([subscription()]); + duplicateKeyRace.subscriptionRepository.save.mockRejectedValueOnce({ + code: 'ER_DUP_ENTRY', + errno: 1062, + }); + await expect( + duplicateKeyRace.service.update('100', { + enabled: true, + name: '更新竞态', + sourceConfig: CONFIG, + sourceKey: SOURCE_KEY, + }), + ).rejects.toMatchObject({ status: HttpStatus.CONFLICT }); + + const infrastructureFailure = new Error('database unavailable'); + const persistenceFailure = setup([subscription()]); + persistenceFailure.subscriptionRepository.save.mockRejectedValueOnce( + infrastructureFailure, + ); + await expect( + persistenceFailure.service.update('100', { + enabled: true, + name: '更新故障', + sourceConfig: CONFIG, + sourceKey: SOURCE_KEY, + }), + ).rejects.toBe(infrastructureFailure); + }); + it('pages only undeleted records with real filters and returns detached, real-time source status', async () => { const source = adapter({ invalidReasonCode: 'ddns_mapping_mismatch',