|
|
@@ -11,6 +11,7 @@ import com.emoon.okr.entity.SysUser;
|
|
|
import com.emoon.okr.enums.PeriodStatus;
|
|
|
import com.emoon.okr.enums.ScoreType;
|
|
|
import com.emoon.okr.exception.BusinessException;
|
|
|
+import com.emoon.okr.event.NotificationEvent;
|
|
|
import com.emoon.okr.mapper.AssessmentPeriodMapper;
|
|
|
import com.emoon.okr.mapper.PeerReviewInvitationMapper;
|
|
|
import com.emoon.okr.mapper.PeerReviewRequestMapper;
|
|
|
@@ -19,6 +20,7 @@ import com.emoon.okr.mapper.PeriodParticipantMapper;
|
|
|
import com.emoon.okr.mapper.SysUserMapper;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.context.ApplicationEventPublisher;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
@@ -38,9 +40,14 @@ public class PeerReviewService {
|
|
|
|
|
|
private static final String ACTIVE = "ACTIVE";
|
|
|
private static final String REQUEST_PENDING_APPROVAL = "PENDING_APPROVAL";
|
|
|
+ private static final String REQUEST_REJECTED = "REJECTED";
|
|
|
+ private static final String REQUEST_IN_PROGRESS = "IN_PROGRESS";
|
|
|
private static final String INVITATION_PROPOSED = "PROPOSED";
|
|
|
+ private static final String INVITATION_PENDING = "PENDING";
|
|
|
+ private static final String INVITATION_ACCEPTED = "ACCEPTED";
|
|
|
private static final String INVITATION_DECLINED = "DECLINED";
|
|
|
private static final String INVITATION_SUBMITTED = "SUBMITTED";
|
|
|
+ private static final String INVITATION_INVALIDATED = "INVALIDATED";
|
|
|
private static final String SAME_DEPARTMENT = "SAME_DEPARTMENT";
|
|
|
private static final String CROSS_DEPARTMENT = "CROSS_DEPARTMENT";
|
|
|
|
|
|
@@ -50,6 +57,7 @@ public class PeerReviewService {
|
|
|
private final PeerReviewRequestMapper requestMapper;
|
|
|
private final PeerReviewInvitationMapper invitationMapper;
|
|
|
private final SysUserMapper userMapper;
|
|
|
+ private final ApplicationEventPublisher publisher;
|
|
|
|
|
|
public List<PeerReviewDto.Candidate> listCandidates(Long periodId, Long subjectUserId) {
|
|
|
AssessmentPeriod period = requireAssessingPeriod(periodId);
|
|
|
@@ -89,9 +97,10 @@ public class PeerReviewService {
|
|
|
if (distinctIds.size() != reviewerIds.size()) {
|
|
|
throw new BusinessException("评价人名单不能重复");
|
|
|
}
|
|
|
- if (requestMapper.selectOne(new LambdaQueryWrapper<PeerReviewRequest>()
|
|
|
+ PeerReviewRequest existingRequest = requestMapper.selectOne(new LambdaQueryWrapper<PeerReviewRequest>()
|
|
|
.eq(PeerReviewRequest::getPeriodId, periodId)
|
|
|
- .eq(PeerReviewRequest::getSubjectUserId, subjectUserId)) != null) {
|
|
|
+ .eq(PeerReviewRequest::getSubjectUserId, subjectUserId));
|
|
|
+ if (existingRequest != null && !REQUEST_REJECTED.equals(existingRequest.getStatus())) {
|
|
|
throw new BusinessException("本周期已提交评价人名单");
|
|
|
}
|
|
|
|
|
|
@@ -123,22 +132,39 @@ public class PeerReviewService {
|
|
|
}
|
|
|
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
- PeerReviewRequest request = new PeerReviewRequest();
|
|
|
+ PeerReviewRequest request = existingRequest == null ? new PeerReviewRequest() : existingRequest;
|
|
|
request.setPeriodId(periodId);
|
|
|
request.setSubjectUserId(subjectUserId);
|
|
|
request.setLeaderId(subject.getEvaluatorId());
|
|
|
request.setStatus(REQUEST_PENDING_APPROVAL);
|
|
|
+ request.setRejectionReason(null);
|
|
|
request.setSubmittedAt(now);
|
|
|
- requestMapper.insert(request);
|
|
|
+ request.setApprovedAt(null);
|
|
|
+ if (existingRequest == null) {
|
|
|
+ requestMapper.insert(request);
|
|
|
+ } else {
|
|
|
+ requestMapper.updateById(request);
|
|
|
+ }
|
|
|
|
|
|
+ Map<Long, PeerReviewInvitation> reusable = existingRequest == null ? Map.of()
|
|
|
+ : invitationMapper.selectList(new LambdaQueryWrapper<PeerReviewInvitation>()
|
|
|
+ .eq(PeerReviewInvitation::getRequestId, request.getId())).stream()
|
|
|
+ .collect(Collectors.toMap(PeerReviewInvitation::getReviewerId, Function.identity()));
|
|
|
List<PeerReviewInvitation> invitations = reviewers.stream().map(reviewer -> {
|
|
|
- PeerReviewInvitation invitation = new PeerReviewInvitation();
|
|
|
+ PeerReviewInvitation invitation = reusable.getOrDefault(
|
|
|
+ reviewer.getUserId(), new PeerReviewInvitation());
|
|
|
invitation.setRequestId(request.getId());
|
|
|
invitation.setReviewerId(reviewer.getUserId());
|
|
|
invitation.setReviewerDepartmentIdSnapshot(reviewer.getDepartmentIdSnapshot());
|
|
|
invitation.setRelationTypeSnapshot(relationType(subject, reviewer));
|
|
|
invitation.setStatus(INVITATION_PROPOSED);
|
|
|
- invitationMapper.insert(invitation);
|
|
|
+ invitation.setDeclineReason(null);
|
|
|
+ invitation.setRespondedAt(null);
|
|
|
+ if (invitation.getId() == null) {
|
|
|
+ invitationMapper.insert(invitation);
|
|
|
+ } else {
|
|
|
+ invitationMapper.updateById(invitation);
|
|
|
+ }
|
|
|
return invitation;
|
|
|
}).toList();
|
|
|
return employeeProgress(request, invitations);
|
|
|
@@ -159,6 +185,140 @@ public class PeerReviewService {
|
|
|
return employeeProgress(request, invitations);
|
|
|
}
|
|
|
|
|
|
+ @Transactional
|
|
|
+ public PeerReviewDto.EmployeeProgress supplementReviewer(Long periodId, Long subjectUserId,
|
|
|
+ Long reviewerId) {
|
|
|
+ AssessmentPeriod period = requireAssessingPeriod(periodId);
|
|
|
+ requireInvitationOpen(period);
|
|
|
+ PeerReviewRequest request = requestMapper.selectOne(new LambdaQueryWrapper<PeerReviewRequest>()
|
|
|
+ .eq(PeerReviewRequest::getPeriodId, periodId)
|
|
|
+ .eq(PeerReviewRequest::getSubjectUserId, subjectUserId));
|
|
|
+ if (request == null || !REQUEST_IN_PROGRESS.equals(request.getStatus())) {
|
|
|
+ throw new BusinessException("当前环评不可补邀");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<PeerReviewInvitation> invitations = new ArrayList<>(invitationMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<PeerReviewInvitation>()
|
|
|
+ .eq(PeerReviewInvitation::getRequestId, request.getId())));
|
|
|
+ if (invitations.size() >= 5) {
|
|
|
+ throw new BusinessException("每周期累计最多 5 人,已无法继续补邀");
|
|
|
+ }
|
|
|
+ if (invitations.stream().noneMatch(item -> INVITATION_DECLINED.equals(item.getStatus())
|
|
|
+ || INVITATION_INVALIDATED.equals(item.getStatus()))) {
|
|
|
+ throw new BusinessException("仅有邀请被拒绝或失效后才可补邀");
|
|
|
+ }
|
|
|
+ if (invitations.stream().anyMatch(item -> Objects.equals(item.getReviewerId(), reviewerId))) {
|
|
|
+ throw new BusinessException("该同事本周期已被邀请,不能重复补邀");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<PeriodParticipant> participants = periodParticipants(periodId);
|
|
|
+ PeriodParticipant subject = requireActiveParticipant(participants, subjectUserId);
|
|
|
+ PeriodParticipant reviewer = participants.stream()
|
|
|
+ .filter(item -> Objects.equals(item.getUserId(), reviewerId))
|
|
|
+ .findFirst().orElse(null);
|
|
|
+ if (reviewer == null || !isEligible(reviewer, subject)) {
|
|
|
+ throw new BusinessException("补邀对象必须是符合条件的本周期有效参与人");
|
|
|
+ }
|
|
|
+
|
|
|
+ PeerReviewInvitation invitation = new PeerReviewInvitation();
|
|
|
+ invitation.setRequestId(request.getId());
|
|
|
+ invitation.setReviewerId(reviewerId);
|
|
|
+ invitation.setReviewerDepartmentIdSnapshot(reviewer.getDepartmentIdSnapshot());
|
|
|
+ invitation.setRelationTypeSnapshot(relationType(subject, reviewer));
|
|
|
+ invitation.setStatus(INVITATION_PROPOSED);
|
|
|
+ invitationMapper.insert(invitation);
|
|
|
+ invitations.add(invitation);
|
|
|
+
|
|
|
+ request.setStatus(REQUEST_PENDING_APPROVAL);
|
|
|
+ request.setSubmittedAt(LocalDateTime.now());
|
|
|
+ request.setApprovedAt(null);
|
|
|
+ requestMapper.updateById(request);
|
|
|
+ publisher.publishEvent(new NotificationEvent(
|
|
|
+ this, request.getLeaderId(), "PEER_REVIEW_LIST_PENDING",
|
|
|
+ "360 环评补邀待审核", "员工补充了一名评价人,请及时审核",
|
|
|
+ "peer_review_request", request.getId()));
|
|
|
+ return employeeProgress(request, invitations);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public PeerReviewDto.LeaderSummary reviewList(Long requestId, Long leaderId,
|
|
|
+ boolean approved, String reason) {
|
|
|
+ PeerReviewRequest request = requirePendingRequest(requestId);
|
|
|
+ if (!Objects.equals(request.getLeaderId(), leaderId)) {
|
|
|
+ throw new BusinessException(403, "仅周期冻结 Leader 可审核评价人名单");
|
|
|
+ }
|
|
|
+ String trimmedReason = reason == null ? "" : reason.trim();
|
|
|
+ if (!approved && trimmedReason.isEmpty()) {
|
|
|
+ throw new BusinessException("驳回评价人名单时必须填写原因");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<PeerReviewInvitation> invitations = invitationMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<PeerReviewInvitation>()
|
|
|
+ .eq(PeerReviewInvitation::getRequestId, requestId));
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ if (approved) {
|
|
|
+ request.setStatus(REQUEST_IN_PROGRESS);
|
|
|
+ request.setApprovedAt(now);
|
|
|
+ request.setRejectionReason(null);
|
|
|
+ for (PeerReviewInvitation invitation : invitations) {
|
|
|
+ if (INVITATION_PROPOSED.equals(invitation.getStatus())) {
|
|
|
+ invitation.setStatus(INVITATION_PENDING);
|
|
|
+ invitationMapper.updateById(invitation);
|
|
|
+ publisher.publishEvent(new NotificationEvent(
|
|
|
+ this, invitation.getReviewerId(), "PEER_REVIEW_INVITED",
|
|
|
+ "收到 360 环评邀请", "你收到一份 360 环评邀请,请及时处理",
|
|
|
+ "peer_review_invitation", invitation.getId()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ request.setStatus(REQUEST_REJECTED);
|
|
|
+ request.setRejectionReason(trimmedReason);
|
|
|
+ for (PeerReviewInvitation invitation : invitations) {
|
|
|
+ if (INVITATION_PROPOSED.equals(invitation.getStatus())) {
|
|
|
+ invitation.setStatus(INVITATION_INVALIDATED);
|
|
|
+ invitationMapper.updateById(invitation);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ publisher.publishEvent(new NotificationEvent(
|
|
|
+ this, request.getSubjectUserId(), "PEER_REVIEW_LIST_REJECTED",
|
|
|
+ "360 环评名单需调整", trimmedReason,
|
|
|
+ "peer_review_request", request.getId()));
|
|
|
+ }
|
|
|
+ requestMapper.updateById(request);
|
|
|
+ return leaderSummary(request);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public PeerReviewDto.InvitationSummary acceptInvitation(Long invitationId, Long reviewerId) {
|
|
|
+ PeerReviewInvitation invitation = requirePendingInvitation(invitationId, reviewerId);
|
|
|
+ invitation.setStatus(INVITATION_ACCEPTED);
|
|
|
+ invitation.setRespondedAt(LocalDateTime.now());
|
|
|
+ invitationMapper.updateById(invitation);
|
|
|
+ return reviewerInvitationSummary(invitation);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public PeerReviewDto.InvitationSummary declineInvitation(Long invitationId, Long reviewerId, String reason) {
|
|
|
+ String trimmedReason = reason == null ? "" : reason.trim();
|
|
|
+ if (trimmedReason.isEmpty()) {
|
|
|
+ throw new BusinessException("拒绝邀请时必须填写原因");
|
|
|
+ }
|
|
|
+ PeerReviewInvitation invitation = requirePendingInvitation(invitationId, reviewerId);
|
|
|
+ invitation.setStatus(INVITATION_DECLINED);
|
|
|
+ invitation.setDeclineReason(trimmedReason);
|
|
|
+ invitation.setRespondedAt(LocalDateTime.now());
|
|
|
+ invitationMapper.updateById(invitation);
|
|
|
+
|
|
|
+ PeerReviewRequest request = requestMapper.selectById(invitation.getRequestId());
|
|
|
+ if (request != null) {
|
|
|
+ publisher.publishEvent(new NotificationEvent(
|
|
|
+ this, request.getSubjectUserId(), "PEER_REVIEW_INVITATION_DECLINED",
|
|
|
+ "360 环评邀请被拒绝", "一名同事拒绝了邀请,请及时补充评价人",
|
|
|
+ "peer_review_request", request.getId()));
|
|
|
+ }
|
|
|
+ return reviewerInvitationSummary(invitation);
|
|
|
+ }
|
|
|
+
|
|
|
private AssessmentPeriod requireAssessingPeriod(Long periodId) {
|
|
|
AssessmentPeriod period = periodMapper.selectById(periodId);
|
|
|
if (period == null || period.getStatus() != PeriodStatus.ASSESSING) {
|
|
|
@@ -167,6 +327,28 @@ public class PeerReviewService {
|
|
|
return period;
|
|
|
}
|
|
|
|
|
|
+ private PeerReviewRequest requirePendingRequest(Long requestId) {
|
|
|
+ PeerReviewRequest request = requestMapper.selectById(requestId);
|
|
|
+ if (request == null) {
|
|
|
+ throw new BusinessException("360 环评请求不存在");
|
|
|
+ }
|
|
|
+ if (!REQUEST_PENDING_APPROVAL.equals(request.getStatus())) {
|
|
|
+ throw new BusinessException("当前评价人名单不可审核");
|
|
|
+ }
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private PeerReviewInvitation requirePendingInvitation(Long invitationId, Long reviewerId) {
|
|
|
+ PeerReviewInvitation invitation = invitationMapper.selectById(invitationId);
|
|
|
+ if (invitation == null || !Objects.equals(invitation.getReviewerId(), reviewerId)) {
|
|
|
+ throw new BusinessException(403, "无权处理该环评邀请");
|
|
|
+ }
|
|
|
+ if (!INVITATION_PENDING.equals(invitation.getStatus())) {
|
|
|
+ throw new BusinessException("当前环评邀请不可处理");
|
|
|
+ }
|
|
|
+ return invitation;
|
|
|
+ }
|
|
|
+
|
|
|
private void requireInvitationOpen(AssessmentPeriod period) {
|
|
|
if (period.getPeerInvitationDeadline() != null
|
|
|
&& LocalDateTime.now().isAfter(period.getPeerInvitationDeadline())) {
|
|
|
@@ -240,4 +422,22 @@ public class PeerReviewService {
|
|
|
}).toList());
|
|
|
return progress;
|
|
|
}
|
|
|
+
|
|
|
+ private PeerReviewDto.LeaderSummary leaderSummary(PeerReviewRequest request) {
|
|
|
+ PeerReviewDto.LeaderSummary summary = new PeerReviewDto.LeaderSummary();
|
|
|
+ summary.setRequestId(request.getId());
|
|
|
+ summary.setSubjectUserId(request.getSubjectUserId());
|
|
|
+ summary.setStatus(request.getStatus());
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private PeerReviewDto.InvitationSummary reviewerInvitationSummary(PeerReviewInvitation invitation) {
|
|
|
+ PeerReviewDto.InvitationSummary summary = new PeerReviewDto.InvitationSummary();
|
|
|
+ summary.setInvitationId(invitation.getId());
|
|
|
+ summary.setReviewerId(invitation.getReviewerId());
|
|
|
+ summary.setRelationType(invitation.getRelationTypeSnapshot());
|
|
|
+ summary.setStatus(invitation.getStatus());
|
|
|
+ summary.setDeclineReason(invitation.getDeclineReason());
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
}
|