|
|
@@ -2,10 +2,12 @@ package com.emoon.okr.service;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.emoon.okr.dto.PeerReviewDto;
|
|
|
+import com.emoon.okr.dto.OkrDetailDto;
|
|
|
import com.emoon.okr.entity.AssessmentPeriod;
|
|
|
import com.emoon.okr.entity.PeerReviewInvitation;
|
|
|
import com.emoon.okr.entity.PeerReviewRequest;
|
|
|
import com.emoon.okr.entity.PerformanceScore;
|
|
|
+import com.emoon.okr.entity.PerformanceScoreItem;
|
|
|
import com.emoon.okr.entity.PeriodParticipant;
|
|
|
import com.emoon.okr.entity.SysUser;
|
|
|
import com.emoon.okr.enums.PeriodStatus;
|
|
|
@@ -16,6 +18,7 @@ import com.emoon.okr.mapper.AssessmentPeriodMapper;
|
|
|
import com.emoon.okr.mapper.PeerReviewInvitationMapper;
|
|
|
import com.emoon.okr.mapper.PeerReviewRequestMapper;
|
|
|
import com.emoon.okr.mapper.PerformanceScoreMapper;
|
|
|
+import com.emoon.okr.mapper.PerformanceScoreItemMapper;
|
|
|
import com.emoon.okr.mapper.PeriodParticipantMapper;
|
|
|
import com.emoon.okr.mapper.SysUserMapper;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
@@ -58,6 +61,8 @@ public class PeerReviewService {
|
|
|
private final PeerReviewInvitationMapper invitationMapper;
|
|
|
private final SysUserMapper userMapper;
|
|
|
private final ApplicationEventPublisher publisher;
|
|
|
+ private final OkrService okrService;
|
|
|
+ private final PerformanceScoreItemMapper scoreItemMapper;
|
|
|
|
|
|
public List<PeerReviewDto.Candidate> listCandidates(Long periodId, Long subjectUserId) {
|
|
|
AssessmentPeriod period = requireAssessingPeriod(periodId);
|
|
|
@@ -240,6 +245,53 @@ public class PeerReviewService {
|
|
|
return employeeProgress(request, invitations);
|
|
|
}
|
|
|
|
|
|
+ public PeerReviewDto.ReviewerWorkspace getReviewerWorkspace(Long invitationId, Long reviewerId) {
|
|
|
+ PeerReviewInvitation invitation = requireReviewerInvitation(invitationId, reviewerId, true);
|
|
|
+ PeerReviewRequest request = requireRequest(invitation.getRequestId());
|
|
|
+
|
|
|
+ PeerReviewDto.ReviewerWorkspace workspace = new PeerReviewDto.ReviewerWorkspace();
|
|
|
+ workspace.setInvitationId(invitation.getId());
|
|
|
+ workspace.setSubjectUserId(request.getSubjectUserId());
|
|
|
+ workspace.setStatus(invitation.getStatus());
|
|
|
+ workspace.setObjectives(okrService.getMyOkrSet(request.getPeriodId(), request.getSubjectUserId())
|
|
|
+ .stream().map(this::objectiveSummary).toList());
|
|
|
+
|
|
|
+ PerformanceScore selfScore = scoreMapper.selectOne(new LambdaQueryWrapper<PerformanceScore>()
|
|
|
+ .eq(PerformanceScore::getPeriodId, request.getPeriodId())
|
|
|
+ .eq(PerformanceScore::getUserId, request.getSubjectUserId())
|
|
|
+ .eq(PerformanceScore::getType, ScoreType.SELF));
|
|
|
+ if (selfScore != null) {
|
|
|
+ workspace.setWorkResults(scoreItemMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<PerformanceScoreItem>()
|
|
|
+ .eq(PerformanceScoreItem::getScoreId, selfScore.getId()))
|
|
|
+ .stream().map(this::workResultSummary).toList());
|
|
|
+ }
|
|
|
+ workspace.setResponse(responseFrom(invitation));
|
|
|
+ return workspace;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public PeerReviewDto.InvitationSummary saveResponse(Long invitationId, Long reviewerId,
|
|
|
+ PeerReviewDto.SaveResponseRequest body,
|
|
|
+ boolean submit) {
|
|
|
+ PeerReviewInvitation invitation = requireReviewerInvitation(invitationId, reviewerId, false);
|
|
|
+ PeerReviewRequest request = requireRequest(invitation.getRequestId());
|
|
|
+ requireReviewOpen(request.getPeriodId());
|
|
|
+ validateResponse(body, submit);
|
|
|
+
|
|
|
+ invitation.setGoalContributionScore(body.getGoalContributionScore());
|
|
|
+ invitation.setCollaborationScore(body.getCollaborationScore());
|
|
|
+ invitation.setAccountabilityScore(body.getAccountabilityScore());
|
|
|
+ invitation.setStrengths(trimToNull(body.getStrengths()));
|
|
|
+ invitation.setImprovementSuggestion(trimToNull(body.getImprovementSuggestion()));
|
|
|
+ if (submit) {
|
|
|
+ invitation.setStatus(INVITATION_SUBMITTED);
|
|
|
+ invitation.setSubmittedAt(LocalDateTime.now());
|
|
|
+ }
|
|
|
+ invitationMapper.updateById(invitation);
|
|
|
+ return reviewerInvitationSummary(invitation);
|
|
|
+ }
|
|
|
+
|
|
|
@Transactional
|
|
|
public PeerReviewDto.LeaderSummary reviewList(Long requestId, Long leaderId,
|
|
|
boolean approved, String reason) {
|
|
|
@@ -338,6 +390,36 @@ public class PeerReviewService {
|
|
|
return request;
|
|
|
}
|
|
|
|
|
|
+ private PeerReviewRequest requireRequest(Long requestId) {
|
|
|
+ PeerReviewRequest request = requestMapper.selectById(requestId);
|
|
|
+ if (request == null) {
|
|
|
+ throw new BusinessException("360 环评请求不存在");
|
|
|
+ }
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private PeerReviewInvitation requireReviewerInvitation(Long invitationId, Long reviewerId,
|
|
|
+ boolean allowSubmitted) {
|
|
|
+ PeerReviewInvitation invitation = invitationMapper.selectById(invitationId);
|
|
|
+ if (invitation == null || !Objects.equals(invitation.getReviewerId(), reviewerId)) {
|
|
|
+ throw new BusinessException(403, "无权查看或填写该环评");
|
|
|
+ }
|
|
|
+ boolean available = INVITATION_ACCEPTED.equals(invitation.getStatus())
|
|
|
+ || allowSubmitted && INVITATION_SUBMITTED.equals(invitation.getStatus());
|
|
|
+ if (!available) {
|
|
|
+ throw new BusinessException("请先接受邀请,或该环评已提交");
|
|
|
+ }
|
|
|
+ return invitation;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void requireReviewOpen(Long periodId) {
|
|
|
+ AssessmentPeriod period = requireAssessingPeriod(periodId);
|
|
|
+ if (period.getPeerReviewDeadline() != null
|
|
|
+ && LocalDateTime.now().isAfter(period.getPeerReviewDeadline())) {
|
|
|
+ throw new BusinessException("360 环评填写已截止");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private PeerReviewInvitation requirePendingInvitation(Long invitationId, Long reviewerId) {
|
|
|
PeerReviewInvitation invitation = invitationMapper.selectById(invitationId);
|
|
|
if (invitation == null || !Objects.equals(invitation.getReviewerId(), reviewerId)) {
|
|
|
@@ -440,4 +522,81 @@ public class PeerReviewService {
|
|
|
summary.setDeclineReason(invitation.getDeclineReason());
|
|
|
return summary;
|
|
|
}
|
|
|
+
|
|
|
+ private PeerReviewDto.ObjectiveSummary objectiveSummary(OkrDetailDto objective) {
|
|
|
+ PeerReviewDto.ObjectiveSummary summary = new PeerReviewDto.ObjectiveSummary();
|
|
|
+ summary.setTitle(objective.getTitle());
|
|
|
+ summary.setProgress(objective.getProgress());
|
|
|
+ if (objective.getKeyResults() != null) {
|
|
|
+ summary.setKeyResults(objective.getKeyResults().stream().map(kr -> {
|
|
|
+ PeerReviewDto.KrSummary item = new PeerReviewDto.KrSummary();
|
|
|
+ item.setTitle(kr.getTitle());
|
|
|
+ item.setCurrentValue(kr.getCurrentValue());
|
|
|
+ item.setTargetValue(kr.getTargetValue());
|
|
|
+ item.setStatus(kr.getStatus());
|
|
|
+ return item;
|
|
|
+ }).toList());
|
|
|
+ }
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private PeerReviewDto.WorkResultSummary workResultSummary(PerformanceScoreItem item) {
|
|
|
+ PeerReviewDto.WorkResultSummary summary = new PeerReviewDto.WorkResultSummary();
|
|
|
+ summary.setName(item.getName());
|
|
|
+ summary.setDescription(item.getDescription());
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ private PeerReviewDto.SaveResponseRequest responseFrom(PeerReviewInvitation invitation) {
|
|
|
+ PeerReviewDto.SaveResponseRequest response = new PeerReviewDto.SaveResponseRequest();
|
|
|
+ response.setGoalContributionScore(invitation.getGoalContributionScore());
|
|
|
+ response.setCollaborationScore(invitation.getCollaborationScore());
|
|
|
+ response.setAccountabilityScore(invitation.getAccountabilityScore());
|
|
|
+ response.setStrengths(invitation.getStrengths());
|
|
|
+ response.setImprovementSuggestion(invitation.getImprovementSuggestion());
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateResponse(PeerReviewDto.SaveResponseRequest body, boolean submit) {
|
|
|
+ if (body == null) {
|
|
|
+ throw new BusinessException("评价内容不能为空");
|
|
|
+ }
|
|
|
+ for (Integer score : List.of(
|
|
|
+ body.getGoalContributionScore() == null ? 0 : body.getGoalContributionScore(),
|
|
|
+ body.getCollaborationScore() == null ? 0 : body.getCollaborationScore(),
|
|
|
+ body.getAccountabilityScore() == null ? 0 : body.getAccountabilityScore())) {
|
|
|
+ if (score != 0 && (score < 1 || score > 5)) {
|
|
|
+ throw new BusinessException("评分必须在 1 至 5 分之间");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ validateMaxLength(body.getStrengths());
|
|
|
+ validateMaxLength(body.getImprovementSuggestion());
|
|
|
+ if (submit && (body.getGoalContributionScore() == null
|
|
|
+ || body.getCollaborationScore() == null
|
|
|
+ || body.getAccountabilityScore() == null)) {
|
|
|
+ throw new BusinessException("正式提交必须完成三项评分");
|
|
|
+ }
|
|
|
+ if (submit && (!validComment(body.getStrengths())
|
|
|
+ || !validComment(body.getImprovementSuggestion()))) {
|
|
|
+ throw new BusinessException("两项简评均须为 10 至 300 个字符");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateMaxLength(String text) {
|
|
|
+ if (text != null && text.trim().length() > 300) {
|
|
|
+ throw new BusinessException("简评不能超过 300 个字符");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean validComment(String text) {
|
|
|
+ int length = text == null ? 0 : text.trim().length();
|
|
|
+ return length >= 10 && length <= 300;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String trimToNull(String text) {
|
|
|
+ if (text == null || text.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return text.trim();
|
|
|
+ }
|
|
|
}
|