|
|
@@ -6,9 +6,11 @@ import com.yimeng.okr.dto.OkrDetailDto.KrWithProgress;
|
|
|
import com.yimeng.okr.entity.*;
|
|
|
import com.yimeng.okr.enums.KrStatus;
|
|
|
import com.yimeng.okr.enums.PeriodStatus;
|
|
|
+import com.yimeng.okr.enums.UserRole;
|
|
|
import com.yimeng.okr.event.NotificationEvent;
|
|
|
import com.yimeng.okr.exception.BusinessException;
|
|
|
import com.yimeng.okr.mapper.*;
|
|
|
+import com.yimeng.okr.security.SecurityUtils;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import org.springframework.context.ApplicationEventPublisher;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
@@ -128,6 +130,67 @@ public class OkrService {
|
|
|
return obj;
|
|
|
}
|
|
|
|
|
|
+ @Transactional
|
|
|
+ public OkrObjective submitHierarchyOkr(Long periodId, Long userId, String title,
|
|
|
+ List<KrKeyResult> krs, String level,
|
|
|
+ Long parentObjectiveId, List<Long> alignedObjectiveIds) {
|
|
|
+ AssessmentPeriod period = periodMapper.selectById(periodId);
|
|
|
+ if (period == null || period.getStatus() != PeriodStatus.OKR_ALIGN) {
|
|
|
+ throw new BusinessException("当前周期不在OKR对齐阶段");
|
|
|
+ }
|
|
|
+ SysUser user = userMapper.selectById(userId);
|
|
|
+ if (user == null) throw new BusinessException(404, "用户不存在");
|
|
|
+
|
|
|
+ if ("COMPANY".equals(level) && !SecurityUtils.hasRole(UserRole.HR_ADMIN)) {
|
|
|
+ throw new BusinessException(403, "仅HR管理员可创建公司级OKR");
|
|
|
+ }
|
|
|
+ if ("DEPARTMENT".equals(level) && !SecurityUtils.hasRole(UserRole.DEPT_LEADER)) {
|
|
|
+ throw new BusinessException(403, "仅部门负责人可创建部门级OKR");
|
|
|
+ }
|
|
|
+
|
|
|
+ if ("INDIVIDUAL".equals(level)) {
|
|
|
+ if (krs == null || krs.size() < 2 || krs.size() > 5) {
|
|
|
+ throw new BusinessException("个人OKR需配置2~5条KR");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ int weightSum = krs.stream().mapToInt(k -> k.getWeight() != null ? k.getWeight() : 0).sum();
|
|
|
+ if (weightSum != 100) throw new BusinessException("KR权重之和必须为100");
|
|
|
+
|
|
|
+ if (parentObjectiveId != null) {
|
|
|
+ OkrObjective parent = objectiveMapper.selectById(parentObjectiveId);
|
|
|
+ if (parent == null) throw new BusinessException(404, "对齐目标不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ OkrObjective obj = new OkrObjective();
|
|
|
+ obj.setPeriodId(periodId);
|
|
|
+ obj.setUserId(userId);
|
|
|
+ obj.setDeptId(user.getDepartmentId());
|
|
|
+ obj.setTitle(title);
|
|
|
+ obj.setLevel(level);
|
|
|
+ obj.setParentObjectiveId(parentObjectiveId);
|
|
|
+ obj.setSortOrder(0);
|
|
|
+ obj.setProgress(0.0);
|
|
|
+ obj.setVersion(1);
|
|
|
+ obj.setStatus("INDIVIDUAL".equals(level) ? "SUBMITTED" : "APPROVED");
|
|
|
+ if (!"INDIVIDUAL".equals(level)) {
|
|
|
+ obj.setReviewedBy(userId);
|
|
|
+ obj.setReviewedAt(LocalDateTime.now());
|
|
|
+ }
|
|
|
+ if (alignedObjectiveIds != null && !alignedObjectiveIds.isEmpty()) {
|
|
|
+ obj.setAlignedObjectiveIds(alignedObjectiveIds.stream()
|
|
|
+ .map(String::valueOf).collect(Collectors.joining(",")));
|
|
|
+ }
|
|
|
+ objectiveMapper.insert(obj);
|
|
|
+
|
|
|
+ for (KrKeyResult kr : krs) {
|
|
|
+ kr.setObjectiveId(obj.getId());
|
|
|
+ kr.setStatus(KrStatus.NOT_STARTED);
|
|
|
+ krMapper.insert(kr);
|
|
|
+ }
|
|
|
+ return obj;
|
|
|
+ }
|
|
|
+
|
|
|
@Transactional
|
|
|
public OkrObjective updateMyOkr(Long objectiveId, Long userId, String title,
|
|
|
List<KrKeyResult> krs, List<Long> alignedObjectiveIds) {
|
|
|
@@ -226,6 +289,21 @@ public class OkrService {
|
|
|
kr.setCurrentValue(newValue);
|
|
|
kr.setStatus(newStatus);
|
|
|
krMapper.updateById(kr);
|
|
|
+
|
|
|
+ // Recalculate O progress
|
|
|
+ List<KrKeyResult> allKrs = krMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<KrKeyResult>().eq(KrKeyResult::getObjectiveId, kr.getObjectiveId()));
|
|
|
+ double totalWeight = allKrs.stream().mapToDouble(k -> k.getWeight() != null ? k.getWeight() : 0).sum();
|
|
|
+ double weightedProgress = 0;
|
|
|
+ if (totalWeight > 0) {
|
|
|
+ for (KrKeyResult k : allKrs) {
|
|
|
+ double krProgress = k.getTargetValue() > 0
|
|
|
+ ? (k.getCurrentValue() / k.getTargetValue()) * 100 : 0;
|
|
|
+ weightedProgress += krProgress * (k.getWeight() / totalWeight);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ obj.setProgress(Math.min(100, Math.round(weightedProgress * 10.0) / 10.0));
|
|
|
+ objectiveMapper.updateById(obj);
|
|
|
}
|
|
|
|
|
|
// --- Queries ---
|
|
|
@@ -351,9 +429,43 @@ public class OkrService {
|
|
|
return List.of(); // deprecated
|
|
|
}
|
|
|
|
|
|
+ public List<OkrDetailDto> getOkrTree(Long periodId) {
|
|
|
+ List<OkrObjective> all = objectiveMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<OkrObjective>().eq(OkrObjective::getPeriodId, periodId));
|
|
|
+ List<OkrDetailDto> enriched = enrichObjectives(all);
|
|
|
+
|
|
|
+ Map<Long, OkrDetailDto> map = enriched.stream()
|
|
|
+ .collect(Collectors.toMap(OkrDetailDto::getId, o -> o, (a, b) -> a));
|
|
|
+ List<OkrDetailDto> roots = new ArrayList<>();
|
|
|
+
|
|
|
+ for (OkrDetailDto dto : enriched) {
|
|
|
+ if (dto.getParentObjectiveId() == null || "COMPANY".equals(dto.getLevel())) {
|
|
|
+ roots.add(dto);
|
|
|
+ } else {
|
|
|
+ OkrDetailDto parent = map.get(dto.getParentObjectiveId());
|
|
|
+ if (parent != null) {
|
|
|
+ if (parent.getChildren() == null) parent.setChildren(new ArrayList<>());
|
|
|
+ parent.getChildren().add(dto);
|
|
|
+ } else {
|
|
|
+ roots.add(dto);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return roots;
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<OkrDetailDto> getOrphanOkrs(Long periodId) {
|
|
|
+ List<OkrObjective> individuals = objectiveMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<OkrObjective>()
|
|
|
+ .eq(OkrObjective::getPeriodId, periodId)
|
|
|
+ .eq(OkrObjective::getLevel, "INDIVIDUAL")
|
|
|
+ .isNull(OkrObjective::getParentObjectiveId));
|
|
|
+ return enrichObjectives(individuals);
|
|
|
+ }
|
|
|
+
|
|
|
// --- Helper ---
|
|
|
|
|
|
- private List<OkrDetailDto> enrichObjectives(List<OkrObjective> objectives) {
|
|
|
+ public List<OkrDetailDto> enrichObjectives(List<OkrObjective> objectives) {
|
|
|
if (objectives.isEmpty()) return List.of();
|
|
|
List<Long> objIds = objectives.stream().map(OkrObjective::getId).toList();
|
|
|
Map<Long, List<KrKeyResult>> krMap = krMapper.selectList(
|
|
|
@@ -400,6 +512,16 @@ public class OkrService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Collect all parent objective IDs for title lookup
|
|
|
+ Set<Long> parentIds = objectives.stream()
|
|
|
+ .map(OkrObjective::getParentObjectiveId)
|
|
|
+ .filter(Objects::nonNull).collect(Collectors.toSet());
|
|
|
+ Map<Long, String> parentTitleMap = new HashMap<>();
|
|
|
+ if (!parentIds.isEmpty()) {
|
|
|
+ objectiveMapper.selectBatchIds(parentIds)
|
|
|
+ .forEach(p -> parentTitleMap.put(p.getId(), p.getTitle()));
|
|
|
+ }
|
|
|
+
|
|
|
List<OkrDetailDto> result = new ArrayList<>();
|
|
|
for (OkrObjective obj : objectives) {
|
|
|
List<KrKeyResult> krs = krMap.getOrDefault(obj.getId(), List.of());
|
|
|
@@ -432,6 +554,9 @@ public class OkrService {
|
|
|
dto.setUserName(userNames.get(obj.getUserId()));
|
|
|
dto.setDeptName(deptNames.get(obj.getDeptId()));
|
|
|
if (obj.getReviewedBy() != null) dto.setReviewerName(userNames.get(obj.getReviewedBy()));
|
|
|
+ if (obj.getParentObjectiveId() != null) {
|
|
|
+ dto.setParentObjectiveTitle(parentTitleMap.get(obj.getParentObjectiveId()));
|
|
|
+ }
|
|
|
if (obj.getAlignedObjectiveIds() != null && !obj.getAlignedObjectiveIds().isEmpty()) {
|
|
|
List<OkrDetailDto> aligned = Arrays.stream(obj.getAlignedObjectiveIds().split(","))
|
|
|
.map(String::trim).filter(s -> !s.isEmpty())
|