package com.labelsys.backend.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.labelsys.backend.common.ResultCode; import com.labelsys.backend.common.exception.BusinessException; import com.labelsys.backend.context.LoginUser; import com.labelsys.backend.dto.request.MergeReviewResultRequest; import com.labelsys.backend.dto.response.MergeReviewResultResponse; import com.labelsys.backend.entity.AnnotationResult; import com.labelsys.backend.entity.AnnotationResultHistory; import com.labelsys.backend.enums.QaContentStorageMode; import com.labelsys.backend.enums.UserPosition; import com.labelsys.backend.mapper.AnnotationResultHistoryMapper; import com.labelsys.backend.mapper.AnnotationResultMapper; import com.labelsys.backend.util.IdGenerator; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Slf4j @Service @RequiredArgsConstructor public class AnnotationResultArchiveService { private static final String MANUAL_ARCHIVE_REASON = "MANUAL_REVIEW"; private final AnnotationResultMapper annotationResultMapper; private final AnnotationResultHistoryMapper annotationResultHistoryMapper; @Value("${labelsys.annotation.auto-archive-timeout:PT2H}") private Duration autoArchiveTimeout; @Transactional public MergeReviewResultResponse mergeReview(LoginUser currentUser, Long resultId, MergeReviewResultRequest request) { assertReviewer(currentUser); AnnotationResult result = annotationResultMapper.findActiveByIdAndCompanyId(resultId, currentUser.companyId()); if (result == null) { throw new BusinessException(ResultCode.NOT_FOUND, "运行态结果不存在"); } LocalDateTime archivedAt = LocalDateTime.now(); AnnotationResultHistory history = AnnotationResultHistory.builder() .id(IdGenerator.nextId()) .companyId(result.getCompanyId()) .creatorId(result.getCreatorId()) .creatorRole(result.getCreatorRole()) .sourceResultId(result.getId()) .taskId(result.getTaskId()) .resourceId(result.getResourceId()) .qaContentJson(request.qaContentJson()) .qaContentStorageMode(resolveStorageMode(result)) .qaContentFilePath(result.getQaContentFilePath()) .archiveReason(MANUAL_ARCHIVE_REASON) .archivedBy(currentUser.userId()) .archivedAt(archivedAt) .build(); annotationResultHistoryMapper.insert(history); int updated = annotationResultMapper.markArchived( result.getId(), currentUser.companyId(), currentUser.userId(), request.reviewComment(), archivedAt); if (updated == 0) { throw new BusinessException(ResultCode.CONFLICT, "结果已被其他操作处理"); } log.info("merged review result, companyId={}, reviewerId={}, resultId={}, historyId={}", currentUser.companyId(), currentUser.userId(), resultId, history.getId()); return new MergeReviewResultResponse(resultId, history.getId(), MANUAL_ARCHIVE_REASON, archivedAt); } @Transactional public int autoArchiveEligibleResults() { LocalDateTime cutoff = LocalDateTime.now().minus(autoArchiveTimeout); List results = annotationResultMapper.selectList(new LambdaQueryWrapper() .eq(AnnotationResult::getIsDeleted, false) .eq(AnnotationResult::getRequiresManualReview, false) .lt(AnnotationResult::getCreatedAt, cutoff)); int archivedCount = 0; for (AnnotationResult result : results) { if (archiveRuntimeResult(result, null, "AUTO_ARCHIVE", null) != null) { archivedCount++; } } return archivedCount; } private void assertReviewer(LoginUser currentUser) { if (currentUser.position() != UserPosition.REVIEWER && currentUser.position() != UserPosition.ADMIN) { throw new BusinessException(ResultCode.FORBIDDEN, "当前用户没有审核权限"); } } private String resolveStorageMode(AnnotationResult result) { if (QaContentStorageMode.isValid(result.getQaContentStorageMode())) { return result.getQaContentStorageMode(); } return QaContentStorageMode.INLINE.name(); } private MergeReviewResultResponse archiveRuntimeResult(AnnotationResult result, Long reviewerId, String archiveReason, String reviewComment) { LocalDateTime archivedAt = LocalDateTime.now(); AnnotationResultHistory history = AnnotationResultHistory.builder() .id(IdGenerator.nextId()) .companyId(result.getCompanyId()) .creatorId(result.getCreatorId()) .creatorRole(result.getCreatorRole()) .sourceResultId(result.getId()) .taskId(result.getTaskId()) .resourceId(result.getResourceId()) .qaContentJson(result.getQaContentJson()) .qaContentStorageMode(resolveStorageMode(result)) .qaContentFilePath(result.getQaContentFilePath()) .archiveReason(archiveReason) .archivedBy(reviewerId) .archivedAt(archivedAt) .build(); annotationResultHistoryMapper.insert(history); int updated = annotationResultMapper.markArchived( result.getId(), result.getCompanyId(), reviewerId, reviewComment, archivedAt); if (updated == 0) { return null; } return new MergeReviewResultResponse(result.getId(), history.getId(), archiveReason, archivedAt); } }