diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java index 380b06ca5..bc6e0b0af 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java @@ -424,7 +424,11 @@ public class SkillPublishService { Skill newSkill = new Skill(namespace.getId(), skillSlug, publisherId, visibility); newSkill.setCreatedBy(publisherId); try { - return skillRepository.save(newSkill); + Skill savedSkill = skillRepository.save(newSkill); + // save() may defer the unique-constraint check until transaction commit. + // Flush here so this boundary can translate the coordinate race. + skillRepository.flush(); + return savedSkill; } catch (DataIntegrityViolationException ex) { // A concurrent publish for the same (namespace, slug, owner) coordinate inserted // the skill first and won the unique-constraint race. Surface a deterministic @@ -486,6 +490,8 @@ public class SkillPublishService { try { version = skillVersionRepository.save(version); + // Detect the version-coordinate race before object storage writes begin. + skillVersionRepository.flush(); } catch (DataIntegrityViolationException ex) { // A concurrent publish inserted the same (skillId, version) coordinate first and won the // unique-constraint race. Surface a deterministic business conflict rather than an HTTP 500. diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java index 67ce616ca..a680e3202 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillPublishServiceTest.java @@ -476,7 +476,7 @@ class SkillPublishServiceTest { verify(reviewTaskRepository).deleteBySkillVersionIdIn(List.of(8L)); verify(skillFileRepository).deleteByVersionId(8L); verify(skillVersionRepository).delete(rejectedVersion); - verify(skillVersionRepository).flush(); + verify(skillVersionRepository, times(2)).flush(); verify(objectStorageService).deleteObjects(List.of("skills/1/8/SKILL.md", "packages/1/8/bundle.zip")); ArgumentCaptor reviewTaskCaptor = ArgumentCaptor.forClass(ReviewTask.class); @@ -1779,13 +1779,15 @@ class SkillPublishServiceTest { when(skillRepository.findByNamespaceIdAndSlugAndOwnerId(any(), eq("test-skill"), eq(publisherId))) .thenReturn(Optional.empty()); // A concurrent publish inserts the same (namespace, slug, owner) coordinate first and wins the race. - when(skillRepository.save(any())).thenThrow(new DataIntegrityViolationException("duplicate key")); + when(skillRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doThrow(new DataIntegrityViolationException("duplicate key")).when(skillRepository).flush(); DomainBadRequestException exception = assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries( namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of())); assertEquals("error.skill.publish.concurrentConflict", exception.messageCode()); assertEquals("test-skill", String.valueOf(exception.messageArgs()[0])); + verify(skillRepository).flush(); verify(skillVersionRepository, never()).save(any(SkillVersion.class)); } @@ -1816,13 +1818,14 @@ class SkillPublishServiceTest { .thenReturn(Optional.of(skill)); when(skillVersionRepository.findBySkillIdAndVersion(any(), eq("1.0.0"))).thenReturn(Optional.empty()); // A concurrent publish inserts the same (skillId, version) coordinate first and wins the race. - when(skillVersionRepository.save(any(SkillVersion.class))) - .thenThrow(new DataIntegrityViolationException("duplicate key")); + when(skillVersionRepository.save(any(SkillVersion.class))).thenAnswer(invocation -> invocation.getArgument(0)); + doThrow(new DataIntegrityViolationException("duplicate key")).when(skillVersionRepository).flush(); DomainBadRequestException exception = assertThrows(DomainBadRequestException.class, () -> service.publishFromEntries( namespaceSlug, entries, publisherId, SkillVisibility.PUBLIC, Set.of())); assertEquals("error.skill.publish.concurrentConflict", exception.messageCode()); + verify(skillVersionRepository).flush(); // Nothing should be uploaded to object storage once the coordinate race is lost. verify(objectStorageService, never()).putObject(anyString(), any(), anyLong(), anyString()); }