package com.mydreamjob.resume; import com.mydreamjob.candidatescore.refresh.CandidateScoringRefreshRequestService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; /** * 简历版本写入工作单元:每个"计算版本号 + 插入"运行在独立事务(REQUIRES_NEW)。 * *

原因:唯一键冲突(uk_resume_versions_user_no)会把 Hibernate 事务标记为 rollback-only, * 在同一事务内重试必然回滚。独立事务保证单次冲突只回滚自身,外层可安全重试。 */ @Component @RequiredArgsConstructor public class ResumeVersionWriter { private final ResumeVersionRepository resumeVersionRepository; /** * R01-D4-2 F3:刷新登记必须与版本行在同一提交边界。 * *

版本行由本组件的 {@code REQUIRES_NEW} 事务独立提交(这是既有的并发冲突重试防护, * 不能移除)。若登记留在外层事务,外层在提交阶段失败就会出现"版本行已落库、刷新信号 * 丢失"。把登记放进本事务后,二者要么一起提交、要么一起回滚。

*/ private final CandidateScoringRefreshRequestService scoringRefreshRequestService; /** 计算用户下一个版本号(max+1)并插入;并发冲突抛 DataIntegrityViolationException */ @Transactional(propagation = Propagation.REQUIRES_NEW) public ResumeVersion saveNewVersion(Long userId, String name, String storagePath, String fileName, String contentType, long fileSize) { Integer maxNo = resumeVersionRepository.findMaxVersionNo(userId); int versionNo = (maxNo == null ? 0 : maxNo) + 1; ResumeVersion version = ResumeVersion.builder() .userId(userId) .name(name) .versionNo(versionNo) .fileStoragePath(storagePath) .fileName(fileName) .contentType(contentType) .fileSize(fileSize) .defaultKey(null) .status(ResumeVersion.STATUS_ACTIVE) .build(); ResumeVersion saved = resumeVersionRepository.save(version); // R01-D4-2 F3:与版本行同事务登记(RESUME 版本数/最近更新影响 GROWTH 维度)。 scoringRefreshRequestService.requestRefresh(userId, LocalDateTime.now()); return saved; } }