fix(review): handle promotion target skill conflicts

This commit is contained in:
dongmucat 2026-04-14 16:23:31 +08:00
parent e35e99ae59
commit 082d97a69d
4 changed files with 51 additions and 1 deletions

View file

@ -167,3 +167,4 @@ validation.auth.password.reset.email.invalid=Email format is invalid
validation.auth.password.reset.code.notBlank=Verification code cannot be blank
validation.auth.password.reset.code.invalid=Verification code must be 6 digits
validation.auth.password.reset.newPassword.notBlank=New password cannot be blank
promotion.target_skill_conflict=The target global skill "{0}" already exists

View file

@ -167,3 +167,4 @@ validation.auth.password.reset.email.invalid=邮箱格式不正确
validation.auth.password.reset.code.notBlank=验证码不能为空
validation.auth.password.reset.code.invalid=验证码必须为 6 位数字
validation.auth.password.reset.newPassword.notBlank=新密码不能为空
promotion.target_skill_conflict=目标全局技能“{0}”已存在

View file

@ -16,6 +16,7 @@ import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
import com.iflytek.skillhub.domain.skill.*;
import jakarta.persistence.EntityManager;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -207,6 +208,8 @@ public class PromotionService {
SkillVersion sourceVersion = skillVersionRepository.findById(approvedRequest.getSourceVersionId())
.orElseThrow(() -> new DomainNotFoundException("skill_version.not_found", approvedRequest.getSourceVersionId()));
assertTargetSkillNotExists(approvedRequest, sourceSkill);
// Create new skill in global namespace
Skill newSkill = new Skill(approvedRequest.getTargetNamespaceId(), sourceSkill.getSlug(),
sourceSkill.getOwnerId(), SkillVisibility.PUBLIC);
@ -215,7 +218,11 @@ public class PromotionService {
newSkill.setSourceSkillId(sourceSkill.getId());
newSkill.setCreatedBy(reviewerId);
newSkill.setUpdatedBy(reviewerId);
newSkill = skillRepository.save(newSkill);
try {
newSkill = skillRepository.save(newSkill);
} catch (DataIntegrityViolationException ex) {
throw duplicateTargetSkillConflict(sourceSkill.getSlug(), ex);
}
// Create new version copying metadata from source
SkillVersion newVersion = new SkillVersion(newSkill.getId(), sourceVersion.getVersion(),
@ -264,6 +271,24 @@ public class PromotionService {
return savedRequest;
}
private void assertTargetSkillNotExists(PromotionRequest approvedRequest, Skill sourceSkill) {
skillRepository.findByNamespaceIdAndSlugAndOwnerId(
approvedRequest.getTargetNamespaceId(),
sourceSkill.getSlug(),
sourceSkill.getOwnerId()
).ifPresent(existing -> {
throw duplicateTargetSkillConflict(sourceSkill.getSlug(), null);
});
}
private DomainBadRequestException duplicateTargetSkillConflict(String slug, Exception cause) {
DomainBadRequestException ex = new DomainBadRequestException("promotion.target_skill_conflict", slug);
if (cause != null) {
ex.initCause(cause);
}
return ex;
}
/**
* Rejects a pending promotion request without changing the source skill.
*/

View file

@ -19,6 +19,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataIntegrityViolationException;
import java.time.Clock;
import java.time.Instant;
@ -511,6 +512,28 @@ class PromotionServiceTest {
() -> promotionService.approvePromotion(PROMOTION_ID, REVIEWER_ID, "ok", Set.of("SKILL_ADMIN")));
}
@Test
void shouldTranslateDuplicateTargetSkillIntoBadRequest() {
PromotionRequest pr = createPendingPromotion();
Skill sourceSkill = createSourceSkill();
SkillVersion sourceVersion = createPublishedVersion();
when(promotionRequestRepository.findById(PROMOTION_ID)).thenReturn(Optional.of(pr));
when(permissionChecker.canReviewPromotion(pr, REVIEWER_ID, Set.of("SKILL_ADMIN"))).thenReturn(true);
when(promotionRequestRepository.updateStatusWithVersion(
PROMOTION_ID, ReviewTaskStatus.APPROVED, REVIEWER_ID, "ok", null, pr.getVersion()))
.thenReturn(1);
when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(sourceSkill));
when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(sourceVersion));
when(skillRepository.save(any(Skill.class)))
.thenThrow(new DataIntegrityViolationException("duplicate key value violates unique constraint"));
DomainBadRequestException ex = assertThrows(DomainBadRequestException.class,
() -> promotionService.approvePromotion(PROMOTION_ID, REVIEWER_ID, "ok", Set.of("SKILL_ADMIN")));
assertEquals("promotion.target_skill_conflict", ex.messageCode());
}
@Test
void shouldCopyDisplayNameAndSummaryToNewSkill() {
PromotionRequest pr = createPendingPromotion();