Browse Source

feat(backend): 新增工作台聚合接口 (WorkspaceController/Service/DTO)

wangkangyjy 2 weeks ago
parent
commit
d125ce578e

+ 22 - 0
backend/src/main/java/com/yimeng/okr/controller/WorkspaceController.java

@@ -0,0 +1,22 @@
+package com.yimeng.okr.controller;
+
+import com.yimeng.okr.dto.ApiResult;
+import com.yimeng.okr.dto.WorkspaceOverviewDto;
+import com.yimeng.okr.security.SecurityUtils;
+import com.yimeng.okr.service.WorkspaceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/workspace")
+@RequiredArgsConstructor
+public class WorkspaceController {
+
+    private final WorkspaceService workspaceService;
+
+    @GetMapping("/overview")
+    public ApiResult<WorkspaceOverviewDto> overview(@RequestParam(required = false) Long periodId) {
+        Long userId = SecurityUtils.getCurrentUserId();
+        return ApiResult.success(workspaceService.getOverview(userId, periodId));
+    }
+}

+ 16 - 0
backend/src/main/java/com/yimeng/okr/dto/WorkflowActionDto.java

@@ -0,0 +1,16 @@
+package com.yimeng.okr.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class WorkflowActionDto {
+    private String action;
+    private String label;
+    private String targetUrl;
+    private Long periodId;
+    private Long objectId;
+}

+ 70 - 0
backend/src/main/java/com/yimeng/okr/dto/WorkspaceOverviewDto.java

@@ -0,0 +1,70 @@
+package com.yimeng.okr.dto;
+
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class WorkspaceOverviewDto {
+    private RecommendedPeriod recommendedPeriod;
+    private List<PeriodOption> availablePeriods;
+    private String currentPhase;
+    private List<TodoItem> myTodos;
+    private TeamSummary teamSummary;
+    private List<RiskItem> risks;
+    private List<DeadlineItem> upcomingDeadlines;
+    private List<WorkflowActionDto> allowedActions;
+
+    @Data
+    public static class RecommendedPeriod {
+        private Long id;
+        private String name;
+        private String status;
+        private String startDate;
+        private String endDate;
+    }
+
+    @Data
+    public static class PeriodOption {
+        private Long id;
+        private String name;
+        private String status;
+    }
+
+    @Data
+    public static class TodoItem {
+        private String type;
+        private String label;
+        private String targetUrl;
+        private Long periodId;
+        private Long objectId;
+        private Integer count;
+        private String urgency;
+    }
+
+    @Data
+    public static class TeamSummary {
+        private int totalMembers;
+        private int okrSubmitted;
+        private int selfScoreSubmitted;
+        private int finalScoreCompleted;
+        private Double avgOkrScore;
+        private Double avgTotalScore;
+    }
+
+    @Data
+    public static class RiskItem {
+        private String type;
+        private String label;
+        private String severity;
+        private Long userId;
+        private String userName;
+        private String detail;
+    }
+
+    @Data
+    public static class DeadlineItem {
+        private String label;
+        private String date;
+        private String type;
+    }
+}

+ 219 - 0
backend/src/main/java/com/yimeng/okr/service/WorkspaceService.java

@@ -0,0 +1,219 @@
+package com.yimeng.okr.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yimeng.okr.dto.WorkflowActionDto;
+import com.yimeng.okr.dto.WorkspaceOverviewDto;
+import com.yimeng.okr.entity.AssessmentPeriod;
+import com.yimeng.okr.enums.PeriodStatus;
+import com.yimeng.okr.mapper.AssessmentPeriodMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WorkspaceService {
+
+    private final JdbcTemplate jdbcTemplate;
+    private final AssessmentPeriodMapper periodMapper;
+
+    public WorkspaceOverviewDto getOverview(Long userId, Long periodId) {
+        AssessmentPeriod period;
+        if (periodId != null) {
+            period = periodMapper.selectById(periodId);
+        } else {
+            period = findRecommendedPeriod();
+        }
+        if (period == null) {
+            return emptyOverview();
+        }
+
+        WorkspaceOverviewDto dto = new WorkspaceOverviewDto();
+
+        WorkspaceOverviewDto.RecommendedPeriod rp = new WorkspaceOverviewDto.RecommendedPeriod();
+        rp.setId(period.getId());
+        rp.setName(period.getName());
+        rp.setStatus(period.getStatus().name());
+        rp.setStartDate(period.getStartDate() != null ? period.getStartDate().toString() : null);
+        rp.setEndDate(period.getEndDate() != null ? period.getEndDate().toString() : null);
+        dto.setRecommendedPeriod(rp);
+
+        List<AssessmentPeriod> allPeriods = periodMapper.selectList(
+                new LambdaQueryWrapper<AssessmentPeriod>()
+                        .ne(AssessmentPeriod::getStatus, PeriodStatus.ARCHIVED)
+                        .orderByDesc(AssessmentPeriod::getStartDate));
+        List<WorkspaceOverviewDto.PeriodOption> options = new ArrayList<>();
+        for (AssessmentPeriod p : allPeriods) {
+            WorkspaceOverviewDto.PeriodOption opt = new WorkspaceOverviewDto.PeriodOption();
+            opt.setId(p.getId());
+            opt.setName(p.getName());
+            opt.setStatus(p.getStatus().name());
+            options.add(opt);
+        }
+        dto.setAvailablePeriods(options);
+        dto.setCurrentPhase(period.getStatus().name());
+        dto.setMyTodos(buildTodos(userId, period));
+        dto.setTeamSummary(buildTeamSummary(userId, period.getId()));
+        dto.setRisks(buildRisks(period.getId()));
+        dto.setUpcomingDeadlines(buildDeadlines(period));
+        dto.setAllowedActions(buildAllowedActions(period));
+
+        return dto;
+    }
+
+    private AssessmentPeriod findRecommendedPeriod() {
+        var periods = periodMapper.selectList(
+                new LambdaQueryWrapper<AssessmentPeriod>()
+                        .in(AssessmentPeriod::getStatus, Arrays.asList(PeriodStatus.OKR_ALIGN, PeriodStatus.EXECUTING, PeriodStatus.ASSESSING))
+                        .orderByDesc(AssessmentPeriod::getStartDate));
+        if (!periods.isEmpty()) return periods.get(0);
+
+        periods = periodMapper.selectList(
+                new LambdaQueryWrapper<AssessmentPeriod>()
+                        .ne(AssessmentPeriod::getStatus, PeriodStatus.ARCHIVED)
+                        .orderByDesc(AssessmentPeriod::getStartDate));
+        return periods.isEmpty() ? null : periods.get(0);
+    }
+
+    private List<WorkspaceOverviewDto.TodoItem> buildTodos(Long userId, AssessmentPeriod period) {
+        List<WorkspaceOverviewDto.TodoItem> todos = new ArrayList<>();
+        String status = period.getStatus().name();
+
+        if ("OKR_ALIGN".equals(status)) {
+            Integer count = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM okr_objective WHERE period_id = ? AND user_id = ? AND (deleted IS NULL OR deleted = 0)",
+                    Integer.class, period.getId(), userId);
+            if (count == null || count == 0) {
+                todos.add(todo("DRAFT_OKR", "制定 OKR", "/okr/my", period.getId()));
+            } else {
+                todos.add(todo("EDIT_OKR", "修改 OKR", "/okr/my", period.getId()));
+            }
+            Integer pending = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM okr_objective o JOIN sys_user u ON o.user_id = u.id " +
+                    "WHERE o.period_id = ? AND u.superior_id = ? AND o.status = 'SUBMITTED'",
+                    Integer.class, period.getId(), userId);
+            if (pending != null && pending > 0) {
+                todos.add(todo("REVIEW_OKR", "审核下属 OKR", "/okr/team", period.getId(), null, pending));
+            }
+        }
+
+        if ("EXECUTING".equals(status)) {
+            Integer krCount = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM kr_key_result k JOIN okr_objective o ON k.objective_id = o.id " +
+                    "WHERE o.period_id = ? AND o.user_id = ? AND k.status != 'COMPLETED'",
+                    Integer.class, period.getId(), userId);
+            if (krCount != null && krCount > 0) {
+                todos.add(todo("UPDATE_PROGRESS", "更新 KR 进度", "/okr/execution", period.getId(), null, krCount));
+            }
+        }
+
+        if ("ASSESSING".equals(status)) {
+            Integer selfCount = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM performance_score WHERE period_id = ? AND user_id = ? AND type = 'SELF'",
+                    Integer.class, period.getId(), userId);
+            if (selfCount == null || selfCount == 0) {
+                todos.add(todo("SELF_SCORE", "提交自评", "/scores/my", period.getId()));
+            }
+            Integer finalCount = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM sys_user WHERE superior_id = ? AND deleted = 0",
+                    Integer.class, userId);
+            if (finalCount != null && finalCount > 0) {
+                Integer pendingFinal = jdbcTemplate.queryForObject(
+                        "SELECT COUNT(*) FROM performance_score s JOIN sys_user u ON s.user_id = u.id " +
+                        "WHERE s.period_id = ? AND u.superior_id = ? AND s.type = 'SELF' AND s.status = 'SUBMITTED'",
+                        Integer.class, period.getId(), userId);
+                if (pendingFinal != null && pendingFinal > 0) {
+                    todos.add(todo("FINAL_SCORE", "完成终评", "/scores/review", period.getId(), null, pendingFinal));
+                }
+            }
+        }
+
+        return todos;
+    }
+
+    private WorkspaceOverviewDto.TodoItem todo(String type, String label, String url, Long periodId) {
+        return todo(type, label, url, periodId, null, null);
+    }
+
+    private WorkspaceOverviewDto.TodoItem todo(String type, String label, String url, Long periodId, Long objectId, Integer count) {
+        WorkspaceOverviewDto.TodoItem item = new WorkspaceOverviewDto.TodoItem();
+        item.setType(type);
+        item.setLabel(label);
+        item.setTargetUrl(url + "?periodId=" + periodId);
+        item.setPeriodId(periodId);
+        item.setObjectId(objectId);
+        item.setCount(count);
+        return item;
+    }
+
+    private WorkspaceOverviewDto.TeamSummary buildTeamSummary(Long userId, Long periodId) {
+        WorkspaceOverviewDto.TeamSummary ts = new WorkspaceOverviewDto.TeamSummary();
+        Integer total = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM sys_user WHERE superior_id = ? AND deleted = 0", Integer.class, userId);
+        ts.setTotalMembers(total != null ? total : 0);
+
+        if (ts.getTotalMembers() > 0) {
+            Integer okrSub = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(DISTINCT o.user_id) FROM okr_objective o JOIN sys_user u ON o.user_id = u.id " +
+                    "WHERE o.period_id = ? AND u.superior_id = ?",
+                    Integer.class, periodId, userId);
+            ts.setOkrSubmitted(okrSub != null ? okrSub : 0);
+        }
+        return ts;
+    }
+
+    private List<WorkspaceOverviewDto.RiskItem> buildRisks(Long periodId) {
+        List<WorkspaceOverviewDto.RiskItem> risks = new ArrayList<>();
+        List<Map<String, Object>> blocked = jdbcTemplate.queryForList(
+                "SELECT k.title, u.real_name FROM kr_key_result k " +
+                "JOIN okr_objective o ON k.objective_id = o.id " +
+                "JOIN sys_user u ON o.user_id = u.id " +
+                "WHERE o.period_id = ? AND k.status = 'BLOCKED' LIMIT 5",
+                periodId);
+        for (Map<String, Object> row : blocked) {
+            WorkspaceOverviewDto.RiskItem risk = new WorkspaceOverviewDto.RiskItem();
+            risk.setType("BLOCKED_KR");
+            risk.setLabel((String) row.get("title"));
+            risk.setSeverity("WARNING");
+            risk.setUserName((String) row.get("real_name"));
+            risks.add(risk);
+        }
+        return risks;
+    }
+
+    private List<WorkspaceOverviewDto.DeadlineItem> buildDeadlines(AssessmentPeriod period) {
+        List<WorkspaceOverviewDto.DeadlineItem> deadlines = new ArrayList<>();
+        if (period.getEndDate() != null) {
+            WorkspaceOverviewDto.DeadlineItem d = new WorkspaceOverviewDto.DeadlineItem();
+            d.setLabel("周期结束");
+            d.setDate(period.getEndDate().toString());
+            long days = ChronoUnit.DAYS.between(LocalDate.now(), period.getEndDate());
+            d.setType(days <= 7 ? "URGENT" : "NORMAL");
+            deadlines.add(d);
+        }
+        return deadlines;
+    }
+
+    private List<WorkflowActionDto> buildAllowedActions(AssessmentPeriod period) {
+        List<WorkflowActionDto> actions = new ArrayList<>();
+        actions.add(new WorkflowActionDto("VIEW_PERIOD", "查看周期详情", "/period", period.getId(), null));
+        actions.add(new WorkflowActionDto("VIEW_OKR", "查看 OKR", "/okr/my", period.getId(), null));
+        actions.add(new WorkflowActionDto("VIEW_SCORES", "查看绩效", "/scores/my", period.getId(), null));
+        return actions;
+    }
+
+    private WorkspaceOverviewDto emptyOverview() {
+        WorkspaceOverviewDto dto = new WorkspaceOverviewDto();
+        dto.setCurrentPhase("NO_PERIOD");
+        dto.setMyTodos(Collections.emptyList());
+        dto.setAvailablePeriods(Collections.emptyList());
+        dto.setAllowedActions(Collections.emptyList());
+        return dto;
+    }
+}