|
|
@@ -4,7 +4,7 @@
|
|
|
|
|
|
**目标:** 在现有 `ASSESSING` 绩效流程中加入员工邀请、Leader 审核、同事评价和两份有效反馈门禁,同时保证原始环评内容只对周期快照 Leader 可见。
|
|
|
|
|
|
-**架构:** 新建独立 `PeerReviewService` 深模块管理请求、邀请、响应、角色 DTO 和门禁;现有 `PerformanceService` 只调用 `requireCanFinalize(periodId, userId, leaderId)`。前端复用“我的绩效”“绩效考评”“周期管理”和通知中心,仅新增评价人填写路由,不新增侧边栏模块。
|
|
|
+**架构:** 新建独立 `PeerReviewService` 深模块管理请求、邀请/评价、角色 DTO 和门禁;现有 `PerformanceService` 只调用 `requireCanFinalize(periodId, userId, leaderId)`。评价草稿与正式内容直接保存在邀请表,沿用项目字符串状态模式;前端复用“我的绩效”“绩效考评”“周期管理”和通知中心,仅新增评价人填写路由。
|
|
|
|
|
|
**技术栈:** Java 17、Spring Boot 3.2、MyBatis-Plus、SQLite、JUnit 5、Mockito、Vue 3、Element Plus、Vite、Vitest。
|
|
|
|
|
|
@@ -14,20 +14,11 @@
|
|
|
|
|
|
### 后端新增文件
|
|
|
|
|
|
-- `backend/src/main/java/com/emoon/okr/enums/PeerReviewRequestStatus.java`:请求流程状态。
|
|
|
-- `backend/src/main/java/com/emoon/okr/enums/PeerReviewInvitationStatus.java`:邀请接受、拒绝、失效和提交状态。
|
|
|
-- `backend/src/main/java/com/emoon/okr/enums/PeerReviewResponseStatus.java`:草稿与正式提交状态。
|
|
|
- `backend/src/main/java/com/emoon/okr/entity/PeerReviewRequest.java`:每位员工每周期唯一的 360 请求。
|
|
|
-- `backend/src/main/java/com/emoon/okr/entity/PeerReviewInvitation.java`:累计邀请记录和拒绝原因。
|
|
|
-- `backend/src/main/java/com/emoon/okr/entity/PeerReviewResponse.java`:三项评分与两项简评。
|
|
|
+- `backend/src/main/java/com/emoon/okr/entity/PeerReviewInvitation.java`:累计邀请、拒绝原因、评价草稿和正式内容。
|
|
|
- `backend/src/main/java/com/emoon/okr/mapper/PeerReviewRequestMapper.java`:请求持久化。
|
|
|
-- `backend/src/main/java/com/emoon/okr/mapper/PeerReviewInvitationMapper.java`:邀请持久化。
|
|
|
-- `backend/src/main/java/com/emoon/okr/mapper/PeerReviewResponseMapper.java`:响应持久化。
|
|
|
-- `backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java`:员工、评价人、Leader、管理员的最小响应 DTO。
|
|
|
-- `backend/src/main/java/com/emoon/okr/dto/request/SubmitPeerReviewListRequest.java`:员工提交候选人 ID。
|
|
|
-- `backend/src/main/java/com/emoon/okr/dto/request/ReviewPeerReviewListRequest.java`:Leader 批准或驳回整份名单。
|
|
|
-- `backend/src/main/java/com/emoon/okr/dto/request/DeclinePeerReviewInvitationRequest.java`:拒绝原因。
|
|
|
-- `backend/src/main/java/com/emoon/okr/dto/request/SavePeerReviewResponseRequest.java`:草稿/提交字段。
|
|
|
+- `backend/src/main/java/com/emoon/okr/mapper/PeerReviewInvitationMapper.java`:邀请与评价持久化。
|
|
|
+- `backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java`:四类角色的最小响应 DTO 和四个嵌套请求类型。
|
|
|
- `backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java`:四类角色 API。
|
|
|
- `backend/src/main/java/com/emoon/okr/service/PeerReviewService.java`:资格、状态、隐私、聚合与终评门禁。
|
|
|
- `backend/src/main/java/com/emoon/okr/schedule/PeerReviewReminderScheduler.java`:截止前 24 小时去重提醒。
|
|
|
@@ -86,7 +77,6 @@ void migrationAddsPeerReviewTablesAndDeadlines() {
|
|
|
assertColumnExists(jdbcTemplate, "assessment_period", "peer_review_deadline");
|
|
|
assertTableExists(jdbcTemplate, "peer_review_request");
|
|
|
assertTableExists(jdbcTemplate, "peer_review_invitation");
|
|
|
- assertTableExists(jdbcTemplate, "peer_review_response");
|
|
|
assertEquals(1, jdbcTemplate.queryForObject(
|
|
|
"SELECT COUNT(*) FROM schema_migration WHERE version = 13", Integer.class));
|
|
|
}
|
|
|
@@ -136,47 +126,44 @@ CREATE TABLE IF NOT EXISTS peer_review_invitation (
|
|
|
relation_type_snapshot VARCHAR(24) NOT NULL,
|
|
|
status VARCHAR(24) NOT NULL DEFAULT 'PROPOSED',
|
|
|
decline_reason TEXT,
|
|
|
- responded_at TEXT,
|
|
|
- created_at TEXT NOT NULL,
|
|
|
- updated_at TEXT NOT NULL,
|
|
|
- deleted INTEGER NOT NULL DEFAULT 0
|
|
|
-);
|
|
|
-CREATE UNIQUE INDEX IF NOT EXISTS uk_peer_review_invitation_reviewer
|
|
|
- ON peer_review_invitation(request_id, reviewer_id);
|
|
|
-CREATE INDEX IF NOT EXISTS idx_peer_review_invitation_reviewer
|
|
|
- ON peer_review_invitation(reviewer_id, status);
|
|
|
-
|
|
|
-CREATE TABLE IF NOT EXISTS peer_review_response (
|
|
|
- id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
- invitation_id INTEGER NOT NULL,
|
|
|
goal_contribution_score INTEGER,
|
|
|
collaboration_score INTEGER,
|
|
|
accountability_score INTEGER,
|
|
|
strengths TEXT,
|
|
|
improvement_suggestion TEXT,
|
|
|
- status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
|
|
|
submitted_at TEXT,
|
|
|
+ responded_at TEXT,
|
|
|
created_at TEXT NOT NULL,
|
|
|
updated_at TEXT NOT NULL,
|
|
|
deleted INTEGER NOT NULL DEFAULT 0
|
|
|
);
|
|
|
-CREATE UNIQUE INDEX IF NOT EXISTS uk_peer_review_response_invitation
|
|
|
- ON peer_review_response(invitation_id);
|
|
|
+CREATE UNIQUE INDEX IF NOT EXISTS uk_peer_review_invitation_reviewer
|
|
|
+ ON peer_review_invitation(request_id, reviewer_id);
|
|
|
+CREATE INDEX IF NOT EXISTS idx_peer_review_invitation_reviewer
|
|
|
+ ON peer_review_invitation(reviewer_id, status);
|
|
|
+
|
|
|
```
|
|
|
|
|
|
迁移必须分别调用 `context.addColumnIfMissing("assessment_period", "peer_invitation_deadline", "TEXT")` 和 `context.addColumnIfMissing("assessment_period", "peer_review_deadline", "TEXT")`,再用 `context.execute` 建表和索引,保证旧数据库可升级。
|
|
|
|
|
|
-- [ ] **步骤 4:创建类型安全的 entity 与 mapper**
|
|
|
+- [ ] **步骤 4:创建最小 entity 与 mapper**
|
|
|
|
|
|
-枚举值固定为:
|
|
|
+状态沿用现有服务字符串常量,不新增 enum:
|
|
|
|
|
|
```java
|
|
|
-public enum PeerReviewRequestStatus { DRAFT, PENDING_APPROVAL, REJECTED, IN_PROGRESS, READY_FOR_FINAL, ARCHIVED }
|
|
|
-public enum PeerReviewInvitationStatus { PROPOSED, PENDING, ACCEPTED, DECLINED, SUBMITTED, INVALIDATED }
|
|
|
-public enum PeerReviewResponseStatus { DRAFT, SUBMITTED }
|
|
|
+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 REQUEST_READY_FOR_FINAL = "READY_FOR_FINAL";
|
|
|
+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";
|
|
|
```
|
|
|
|
|
|
-三个实体使用 `@TableName`、`@TableId(type = IdType.AUTO)`、`@TableLogic` 和现有时间自动填充模式;`AssessmentPeriod` 增加:
|
|
|
+两个实体使用 `@TableName`、`@TableId(type = IdType.AUTO)`、`@TableLogic` 和现有时间自动填充模式;`AssessmentPeriod` 增加:
|
|
|
|
|
|
```java
|
|
|
private LocalDateTime peerInvitationDeadline;
|
|
|
@@ -198,7 +185,7 @@ mvn test
|
|
|
- [ ] **步骤 6:Commit**
|
|
|
|
|
|
```bash
|
|
|
-git add backend/src/main/resources/schema.sql backend/src/main/java/com/emoon/okr/config/DatabaseMigrationRunner.java backend/src/test/java/com/emoon/okr/config/DatabaseMigrationRunnerTest.java backend/src/main/java/com/emoon/okr/entity/AssessmentPeriod.java backend/src/main/java/com/emoon/okr/enums/PeerReviewRequestStatus.java backend/src/main/java/com/emoon/okr/enums/PeerReviewInvitationStatus.java backend/src/main/java/com/emoon/okr/enums/PeerReviewResponseStatus.java backend/src/main/java/com/emoon/okr/entity/PeerReviewRequest.java backend/src/main/java/com/emoon/okr/entity/PeerReviewInvitation.java backend/src/main/java/com/emoon/okr/entity/PeerReviewResponse.java backend/src/main/java/com/emoon/okr/mapper/PeerReviewRequestMapper.java backend/src/main/java/com/emoon/okr/mapper/PeerReviewInvitationMapper.java backend/src/main/java/com/emoon/okr/mapper/PeerReviewResponseMapper.java
|
|
|
+git add backend/src/main/resources/schema.sql backend/src/main/java/com/emoon/okr/config/DatabaseMigrationRunner.java backend/src/test/java/com/emoon/okr/config/DatabaseMigrationRunnerTest.java backend/src/main/java/com/emoon/okr/entity/AssessmentPeriod.java backend/src/main/java/com/emoon/okr/entity/PeerReviewRequest.java backend/src/main/java/com/emoon/okr/entity/PeerReviewInvitation.java backend/src/main/java/com/emoon/okr/mapper/PeerReviewRequestMapper.java backend/src/main/java/com/emoon/okr/mapper/PeerReviewInvitationMapper.java
|
|
|
git commit -m "feat(环评): 添加 360 环评持久化模型"
|
|
|
```
|
|
|
|
|
|
@@ -206,7 +193,6 @@ git commit -m "feat(环评): 添加 360 环评持久化模型"
|
|
|
|
|
|
**文件:**
|
|
|
- 创建:`backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java`
|
|
|
-- 创建:`backend/src/main/java/com/emoon/okr/dto/request/SubmitPeerReviewListRequest.java`
|
|
|
- 创建:`backend/src/main/java/com/emoon/okr/service/PeerReviewService.java`
|
|
|
- 创建:`backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java`
|
|
|
|
|
|
@@ -258,11 +244,11 @@ mvn -Dtest=PeerReviewServiceTest test
|
|
|
|
|
|
- [ ] **步骤 3:定义最小 DTO 与请求**
|
|
|
|
|
|
-`SubmitPeerReviewListRequest`:
|
|
|
+`PeerReviewDto.SubmitListRequest`:
|
|
|
|
|
|
```java
|
|
|
@Data
|
|
|
-public class SubmitPeerReviewListRequest {
|
|
|
+public static class SubmitListRequest {
|
|
|
@NotNull
|
|
|
private Long periodId;
|
|
|
|
|
|
@@ -298,15 +284,13 @@ mvn -Dtest=PeerReviewServiceTest test
|
|
|
- [ ] **步骤 6:Commit**
|
|
|
|
|
|
```bash
|
|
|
-git add backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java backend/src/main/java/com/emoon/okr/dto/request/SubmitPeerReviewListRequest.java backend/src/main/java/com/emoon/okr/service/PeerReviewService.java backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java
|
|
|
+git add backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java backend/src/main/java/com/emoon/okr/service/PeerReviewService.java backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java
|
|
|
git commit -m "feat(环评): 实现评价人资格与邀请名单"
|
|
|
```
|
|
|
|
|
|
## 任务 3:Leader 审核、邀请接受与拒绝补邀
|
|
|
|
|
|
**文件:**
|
|
|
-- 创建:`backend/src/main/java/com/emoon/okr/dto/request/ReviewPeerReviewListRequest.java`
|
|
|
-- 创建:`backend/src/main/java/com/emoon/okr/dto/request/DeclinePeerReviewInvitationRequest.java`
|
|
|
- 创建:`backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java`
|
|
|
- 修改:`backend/src/main/java/com/emoon/okr/service/PeerReviewService.java`
|
|
|
- 修改:`backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java`
|
|
|
@@ -323,7 +307,7 @@ void approveReviewerListRequiresFrozenLeaderAndCreatesPendingInvitations() {
|
|
|
service.reviewList(20L, 9L, true, "");
|
|
|
|
|
|
verify(invitationMapper, times(3)).updateById(argThat(invitation ->
|
|
|
- invitation.getStatus() == PeerReviewInvitationStatus.PENDING));
|
|
|
+ "PENDING".equals(invitation.getStatus())));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -334,7 +318,7 @@ void declineRequiresReasonAndLeavesReviewerIdentityVisibleOnlyToSubjectAndLeader
|
|
|
service.declineInvitation(30L, 3L, "合作接触不足");
|
|
|
|
|
|
verify(invitationMapper).updateById(argThat(invitation ->
|
|
|
- invitation.getStatus() == PeerReviewInvitationStatus.DECLINED
|
|
|
+ "DECLINED".equals(invitation.getStatus())
|
|
|
&& "合作接触不足".equals(invitation.getDeclineReason())));
|
|
|
}
|
|
|
```
|
|
|
@@ -382,7 +366,7 @@ public class PeerReviewController {
|
|
|
|
|
|
@PutMapping("/leader/requests/{id}/review")
|
|
|
public ApiResult<PeerReviewDto.LeaderSummary> review(@PathVariable Long id,
|
|
|
- @Valid @RequestBody ReviewPeerReviewListRequest request) {
|
|
|
+ @Valid @RequestBody PeerReviewDto.ReviewListRequest request) {
|
|
|
return ApiResult.success(peerReviewService.reviewList(
|
|
|
id, SecurityUtils.getCurrentUserId(), request.getApproved(), request.getReason()));
|
|
|
}
|
|
|
@@ -404,14 +388,13 @@ mvn -DskipTests package
|
|
|
- [ ] **步骤 6:Commit**
|
|
|
|
|
|
```bash
|
|
|
-git add backend/src/main/java/com/emoon/okr/dto/request/ReviewPeerReviewListRequest.java backend/src/main/java/com/emoon/okr/dto/request/DeclinePeerReviewInvitationRequest.java backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java backend/src/main/java/com/emoon/okr/service/PeerReviewService.java backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java
|
|
|
+git add backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java backend/src/main/java/com/emoon/okr/service/PeerReviewService.java backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java
|
|
|
git commit -m "feat(环评): 实现名单审核与邀请响应"
|
|
|
```
|
|
|
|
|
|
## 任务 4:限定材料、草稿与正式评价
|
|
|
|
|
|
**文件:**
|
|
|
-- 创建:`backend/src/main/java/com/emoon/okr/dto/request/SavePeerReviewResponseRequest.java`
|
|
|
- 修改:`backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java`
|
|
|
- 修改:`backend/src/main/java/com/emoon/okr/service/PeerReviewService.java`
|
|
|
- 修改:`backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java`
|
|
|
@@ -439,7 +422,7 @@ void reviewerMaterialContainsOkrAndDescriptionsButNoPerformanceScores() {
|
|
|
@Test
|
|
|
void submitResponseRequiresThreeScoresAndTwoTenCharacterComments() {
|
|
|
givenAcceptedInvitation(30L, 20L, 3L);
|
|
|
- SavePeerReviewResponseRequest request = validResponse();
|
|
|
+ PeerReviewDto.SaveResponseRequest request = validResponse();
|
|
|
request.setStrengths("太短");
|
|
|
|
|
|
BusinessException error = assertThrows(BusinessException.class,
|
|
|
@@ -461,11 +444,11 @@ mvn -Dtest=PeerReviewServiceTest test
|
|
|
|
|
|
- [ ] **步骤 3:实现请求验证和最小 Reviewer DTO**
|
|
|
|
|
|
-`SavePeerReviewResponseRequest`:
|
|
|
+`PeerReviewDto.SaveResponseRequest`:
|
|
|
|
|
|
```java
|
|
|
@Data
|
|
|
-public class SavePeerReviewResponseRequest {
|
|
|
+public static class SaveResponseRequest {
|
|
|
@Min(1) @Max(5)
|
|
|
private Integer goalContributionScore;
|
|
|
@Min(1) @Max(5)
|
|
|
@@ -488,10 +471,10 @@ public class SavePeerReviewResponseRequest {
|
|
|
```java
|
|
|
public PeerReviewDto.ReviewerWorkspace getReviewerWorkspace(Long invitationId, Long reviewerId)
|
|
|
public PeerReviewDto.InvitationSummary saveResponse(Long invitationId, Long reviewerId,
|
|
|
- SavePeerReviewResponseRequest body, boolean submit)
|
|
|
+ PeerReviewDto.SaveResponseRequest body, boolean submit)
|
|
|
```
|
|
|
|
|
|
-材料 DTO 仅复制 `OkrDetailDto` 的目标、KR 标题、当前值、目标值、状态和进度,以及 SELF `PerformanceScoreItem` 的 `name`、`description`;禁止复用 `ScoringDto` 或实体作为响应。正式提交后 `response.status=SUBMITTED`、`invitation.status=SUBMITTED`,再次提交返回业务错误。
|
|
|
+材料 DTO 仅复制 `OkrDetailDto` 的目标、KR 标题、当前值、目标值、状态和进度,以及 SELF `PerformanceScoreItem` 的 `name`、`description`;禁止复用 `ScoringDto` 或实体作为响应。草稿直接更新邀请表的可空评价字段;正式提交后 `invitation.status=SUBMITTED` 并写入 `submittedAt`,再次提交返回业务错误。
|
|
|
|
|
|
- [ ] **步骤 5:运行测试**
|
|
|
|
|
|
@@ -505,7 +488,7 @@ mvn -Dtest=PeerReviewServiceTest test
|
|
|
- [ ] **步骤 6:Commit**
|
|
|
|
|
|
```bash
|
|
|
-git add backend/src/main/java/com/emoon/okr/dto/request/SavePeerReviewResponseRequest.java backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java backend/src/main/java/com/emoon/okr/service/PeerReviewService.java backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java
|
|
|
+git add backend/src/main/java/com/emoon/okr/dto/PeerReviewDto.java backend/src/main/java/com/emoon/okr/service/PeerReviewService.java backend/src/main/java/com/emoon/okr/controller/PeerReviewController.java backend/src/test/java/com/emoon/okr/service/PeerReviewServiceTest.java
|
|
|
git commit -m "feat(环评): 实现同事评价与隐私材料"
|
|
|
```
|
|
|
|