|
|
@@ -0,0 +1,1109 @@
|
|
|
+# 手术观测台全链路重构实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 将全部 Web 页面统一为“手术观测台”设计,并以前端工作流为主导补齐工作台、OKR、绩效、历史查询和无损数据迁移能力。
|
|
|
+
|
|
|
+**架构:** 前端增加全局工作区上下文、双层壳层和四类通用页面组件;后端增加聚合查询和明确 DTO,保留旧接口作为兼容层。评分明细通过新表结构化存储,旧 JSON 数据在自动备份后事务化回填。
|
|
|
+
|
|
|
+**技术栈:** Vue 3、Vite、Pinia、Element Plus、Vitest、Playwright、Spring Boot 3.2、MyBatis-Plus、SQLite、JUnit 5、Mockito。
|
|
|
+
|
|
|
+**设计规格:** `docs/superpowers/specs/2026-07-06-surgical-observatory-fullstack-redesign.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+### 前端新增
|
|
|
+
|
|
|
+- `frontend/src/stores/workspace.js`:全局周期、阶段、待办和允许操作。
|
|
|
+- `frontend/src/api/client.js`:Axios 实例和统一错误规范化。
|
|
|
+- `frontend/src/api/workspace.js`:工作台接口。
|
|
|
+- `frontend/src/api/okr.js`:OKR 接口。
|
|
|
+- `frontend/src/api/scoring.js`:评分、沟通、申诉接口。
|
|
|
+- `frontend/src/components/shell/AppShell.vue`:整体布局。
|
|
|
+- `frontend/src/components/shell/ModuleRail.vue`:一级模块导航。
|
|
|
+- `frontend/src/components/shell/ContextNav.vue`:模块内导航。
|
|
|
+- `frontend/src/components/shell/SystemStatusBar.vue`:周期、通知和账户状态。
|
|
|
+- `frontend/src/components/common/PageContextBar.vue`:页面上下文。
|
|
|
+- `frontend/src/components/common/WorkflowRail.vue`:阶段轨道。
|
|
|
+- `frontend/src/components/common/StatusLabel.vue`:状态点和文字。
|
|
|
+- `frontend/src/components/common/MetricBar.vue`:指标进度。
|
|
|
+- `frontend/src/components/common/AsyncState.vue`:加载、空、错误和权限状态。
|
|
|
+- `frontend/src/components/workbench/SplitWorkbench.vue`:列表、主区、检查器。
|
|
|
+- `frontend/src/components/workbench/InspectorPanel.vue`:右侧详情。
|
|
|
+- `frontend/src/components/workbench/StickyActionBar.vue`:底部提交栏。
|
|
|
+- `frontend/src/views/workspace/WorkspaceView.vue`:登录后工作台。
|
|
|
+- `frontend/src/views/okr/OkrExecutionView.vue`:执行进度。
|
|
|
+- `frontend/src/__tests__/`:Vitest 测试。
|
|
|
+- `frontend/e2e/`:Playwright 关键链路测试。
|
|
|
+
|
|
|
+### 后端新增
|
|
|
+
|
|
|
+- `backend/src/main/java/com/yimeng/okr/controller/WorkspaceController.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/service/WorkspaceService.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/WorkspaceOverviewDto.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/OkrWorkbenchDto.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/WorkflowActionDto.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/request/SaveOkrDraftRequest.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/request/ReviewOkrRequest.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/request/UpdateKrProgressRequest.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/request/SubmitScoreRequest.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/entity/PerformanceScoreItem.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/mapper/PerformanceScoreItemMapper.java`
|
|
|
+- `backend/src/test/java/com/yimeng/okr/service/WorkspaceServiceTest.java`
|
|
|
+
|
|
|
+### 主要修改
|
|
|
+
|
|
|
+- `frontend/src/router/index.js`
|
|
|
+- `frontend/src/components/MainLayout.vue`
|
|
|
+- `frontend/src/styles/theme.css`
|
|
|
+- `frontend/src/styles/components.css`
|
|
|
+- `frontend/src/styles/element-plus.css`
|
|
|
+- `frontend/src/views/**/*.vue`
|
|
|
+- `backend/src/main/resources/schema.sql`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/config/DatabaseMigrationRunner.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/controller/OkrController.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/controller/PerformanceController.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/service/OkrService.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/service/PerformanceService.java`
|
|
|
+- `backend/src/main/java/com/yimeng/okr/dto/ScoringDto.java`
|
|
|
+
|
|
|
+## 阶段 0:建立可验证基线
|
|
|
+
|
|
|
+### 任务 1:前端测试基础设施
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/package.json`
|
|
|
+- 修改:`frontend/vite.config.js`
|
|
|
+- 创建:`frontend/src/__tests__/setup.js`
|
|
|
+- 创建:`frontend/src/__tests__/test-baseline.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:安装测试依赖并配置 Vitest**
|
|
|
+
|
|
|
+```js
|
|
|
+import { describe, expect, it } from 'vitest'
|
|
|
+
|
|
|
+describe('frontend test baseline', () => {
|
|
|
+ it('runs in jsdom', () => {
|
|
|
+ expect(document.createElement('div')).toBeInstanceOf(HTMLElement)
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+cd frontend
|
|
|
+npm install -D vitest @vue/test-utils jsdom
|
|
|
+```
|
|
|
+
|
|
|
+在 `vite.config.js` 增加:
|
|
|
+
|
|
|
+```js
|
|
|
+test: {
|
|
|
+ environment: 'jsdom',
|
|
|
+ setupFiles: ['./src/__tests__/setup.js']
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+在 `package.json` 增加:
|
|
|
+
|
|
|
+```json
|
|
|
+"test": "vitest run"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行测试并确认基础设施可用**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **步骤 3:提交测试基线**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/package.json frontend/package-lock.json frontend/vite.config.js frontend/src/__tests__/setup.js frontend/src/__tests__/test-baseline.test.js
|
|
|
+git commit -m "test(frontend): 建立界面重构测试基线"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 2:记录当前构建和评分基线
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 验证:`frontend/scripts/verify-medtech-theme.mjs`
|
|
|
+- 验证:`frontend/scripts/verify-scoring-contract.mjs`
|
|
|
+
|
|
|
+- [ ] **步骤 1:运行当前守卫**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd frontend
|
|
|
+node scripts/verify-medtech-theme.mjs
|
|
|
+node scripts/verify-scoring-contract.mjs
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+预期:两个守卫通过,生产构建退出码为 0。
|
|
|
+
|
|
|
+- [ ] **步骤 2:在实施记录中保存三条命令的通过输出**
|
|
|
+
|
|
|
+此任务不修改文件、不创建提交。若任一命令失败,先用 `systematic-debugging` 查明当前基线问题,再开始阶段 1。
|
|
|
+
|
|
|
+## 阶段 1:迁移安全与基础壳层
|
|
|
+
|
|
|
+### 任务 3:数据库迁移备份与事务
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/config/DatabaseMigrationRunner.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/service/DatabaseBackupService.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/config/DatabaseMigrationRunnerTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:增加失败测试**
|
|
|
+
|
|
|
+```java
|
|
|
+@Test
|
|
|
+void shouldRollbackMigrationAndNotRecordVersionWhenBackfillFails() {
|
|
|
+ assertThrows(RuntimeException.class, () -> runner.run());
|
|
|
+ assertEquals(0, jdbcTemplate.queryForObject(
|
|
|
+ "SELECT COUNT(*) FROM schema_migration WHERE version = 3", Integer.class));
|
|
|
+ assertFalse(jdbcTemplate.queryForList("PRAGMA table_info(performance_score)").stream()
|
|
|
+ .anyMatch(row -> "migration_probe".equalsIgnoreCase(String.valueOf(row.get("name")))));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行测试确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend
|
|
|
+mvn -Dtest=DatabaseMigrationRunnerTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:使用 `TransactionTemplate` 包裹单个迁移**
|
|
|
+
|
|
|
+```java
|
|
|
+transactionTemplate.executeWithoutResult(status -> {
|
|
|
+ migration.apply(context);
|
|
|
+ jdbcTemplate.update(
|
|
|
+ "INSERT INTO schema_migration(version, description, applied_at) VALUES (?, ?, datetime('now'))",
|
|
|
+ migration.version(), migration.description());
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+检测到待执行迁移时先调用:
|
|
|
+
|
|
|
+```java
|
|
|
+databaseBackupService.createBackup("pre-migration-v" + firstPendingVersion);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:验证回滚和备份测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=DatabaseMigrationRunnerTest,DatabaseBackupServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/src/main/java/com/yimeng/okr/config/DatabaseMigrationRunner.java backend/src/main/java/com/yimeng/okr/service/DatabaseBackupService.java backend/src/test/java/com/yimeng/okr/config/DatabaseMigrationRunnerTest.java
|
|
|
+git commit -m "feat(backend): 为数据库迁移增加备份和事务回滚"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 4:评分明细表与旧 JSON 回填
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`backend/src/main/resources/schema.sql`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/config/DatabaseMigrationRunner.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/entity/PerformanceScoreItem.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/mapper/PerformanceScoreItemMapper.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/config/DatabaseMigrationRunnerTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写旧数据回填测试**
|
|
|
+
|
|
|
+插入带有 `krScores` 和 `dimensions` 的 `comments_json`,运行迁移 3,断言:
|
|
|
+
|
|
|
+```java
|
|
|
+assertEquals(5, count("performance_score_item", "score_id = 1"));
|
|
|
+assertEquals(1, count("schema_migration", "version = 3"));
|
|
|
+assertEquals(originalJson, queryString("performance_score", "comments_json", 1L));
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行并确认表不存在**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=DatabaseMigrationRunnerTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:增加表**
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE IF NOT EXISTS performance_score_item (
|
|
|
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
+ score_id INTEGER NOT NULL,
|
|
|
+ item_type VARCHAR(20) NOT NULL,
|
|
|
+ reference_id INTEGER,
|
|
|
+ name VARCHAR(256) NOT NULL,
|
|
|
+ max_score REAL,
|
|
|
+ score REAL NOT NULL DEFAULT 0,
|
|
|
+ description TEXT,
|
|
|
+ sort_order INTEGER DEFAULT 0,
|
|
|
+ snapshot_json TEXT,
|
|
|
+ created_at TEXT NOT NULL,
|
|
|
+ updated_at TEXT NOT NULL
|
|
|
+);
|
|
|
+CREATE INDEX IF NOT EXISTS idx_score_item_score ON performance_score_item(score_id, sort_order);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:在迁移 3 中用 Jackson 解析并回填**
|
|
|
+
|
|
|
+无法解析时记录 `score_id`,保留 JSON,不插入不完整明细。回填结束后校验可解析评分的明细数量和总分;不一致时抛出异常并回滚。
|
|
|
+
|
|
|
+同时补充并由迁移测试验证以下索引:
|
|
|
+
|
|
|
+- `performance_score(period_id, user_id, type)`
|
|
|
+- `performance_score(period_id, status)`
|
|
|
+- `performance_score_item(score_id, sort_order)`
|
|
|
+- `okr_objective(period_id, user_id)`
|
|
|
+- `kr_key_result(source_kr_id)`
|
|
|
+- `notification(user_id, is_read)`
|
|
|
+
|
|
|
+- [ ] **步骤 5:运行迁移和性能服务测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=DatabaseMigrationRunnerTest,PerformanceServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+预期:PASS。
|
|
|
+
|
|
|
+- [ ] **步骤 6:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/src/main/resources/schema.sql backend/src/main/java/com/yimeng/okr/config/DatabaseMigrationRunner.java backend/src/main/java/com/yimeng/okr/entity/PerformanceScoreItem.java backend/src/main/java/com/yimeng/okr/mapper/PerformanceScoreItemMapper.java backend/src/test/java/com/yimeng/okr/config/DatabaseMigrationRunnerTest.java
|
|
|
+git commit -m "feat(backend): 结构化保存评分明细并兼容历史数据"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 5:视觉 token、字体和基础组件
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/styles/theme.css`
|
|
|
+- 修改:`frontend/src/styles/components.css`
|
|
|
+- 修改:`frontend/src/styles/element-plus.css`
|
|
|
+- 创建:`frontend/src/assets/fonts/SourceHanSansSC-Regular.woff2`
|
|
|
+- 创建:`frontend/src/assets/fonts/SourceHanSansSC-Medium.woff2`
|
|
|
+- 创建:`frontend/src/assets/fonts/IBMPlexSans-Regular.woff2`
|
|
|
+- 创建:`frontend/src/assets/fonts/LICENSE-SourceHanSans.txt`
|
|
|
+- 创建:`frontend/src/assets/fonts/LICENSE-IBMPlex.txt`
|
|
|
+- 创建:`frontend/src/components/common/StatusLabel.vue`
|
|
|
+- 创建:`frontend/src/components/common/MetricBar.vue`
|
|
|
+- 创建:`frontend/src/components/common/AsyncState.vue`
|
|
|
+- 测试:`frontend/src/__tests__/theme-contract.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写并运行失败的主题契约测试**
|
|
|
+
|
|
|
+```js
|
|
|
+import { readFileSync } from 'node:fs'
|
|
|
+import { describe, expect, it } from 'vitest'
|
|
|
+
|
|
|
+describe('surgical observatory theme', () => {
|
|
|
+ it('defines shell and clinical workspace tokens', () => {
|
|
|
+ const css = readFileSync(new URL('../styles/theme.css', import.meta.url), 'utf8')
|
|
|
+ expect(css).toContain('--shell-rail-width:')
|
|
|
+ expect(css).toContain('--shell-context-width:')
|
|
|
+ expect(css).toContain('--workspace-bg:')
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+cd frontend
|
|
|
+npx vitest run src/__tests__/theme-contract.test.js
|
|
|
+```
|
|
|
+
|
|
|
+预期:FAIL,缺少 `--shell-rail-width`。
|
|
|
+
|
|
|
+- [ ] **步骤 2:增加 token**
|
|
|
+
|
|
|
+```css
|
|
|
+:root {
|
|
|
+ --shell-rail-width: 64px;
|
|
|
+ --shell-context-width: 176px;
|
|
|
+ --shell-status-height: 36px;
|
|
|
+ --page-context-height: 64px;
|
|
|
+ --workspace-bg: #f5f8fa;
|
|
|
+ --workspace-surface: #ffffff;
|
|
|
+ --shell-bg-deep: #120d4a;
|
|
|
+ --shell-bg: #1d166b;
|
|
|
+ --motion-fast: 160ms ease;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:加入本地字体及许可证并实现三个基础组件**
|
|
|
+
|
|
|
+从 Adobe Source Han Sans 和 IBM Plex 的官方发行包中提取上述 WOFF2 文件,保留对应 OFL 许可证,并在 `theme.css` 使用 `@font-face` 引用;提交前运行 `file frontend/src/assets/fonts/*.woff2` 确认均为 Web Open Font Format。`StatusLabel` 接受 `tone` 和 `label`;`MetricBar` 接受 `value`、`max` 和 `tone`;`AsyncState` 接受 `loading`、`error`、`empty` 和重试事件。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试和主题守卫**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+node scripts/verify-medtech-theme.mjs
|
|
|
+```
|
|
|
+
|
|
|
+预期:主题 token 测试通过;壳层断言仍失败。
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/assets/fonts/SourceHanSansSC-Regular.woff2 frontend/src/assets/fonts/SourceHanSansSC-Medium.woff2 frontend/src/assets/fonts/IBMPlexSans-Regular.woff2 frontend/src/assets/fonts/LICENSE-SourceHanSans.txt frontend/src/assets/fonts/LICENSE-IBMPlex.txt frontend/src/styles/theme.css frontend/src/styles/components.css frontend/src/styles/element-plus.css frontend/src/components/common/StatusLabel.vue frontend/src/components/common/MetricBar.vue frontend/src/components/common/AsyncState.vue frontend/src/__tests__/theme-contract.test.js
|
|
|
+git commit -m "feat(frontend): 建立手术观测台视觉基础"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 6:工作台聚合接口
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/controller/WorkspaceController.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/service/WorkspaceService.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/WorkspaceOverviewDto.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/WorkflowActionDto.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/service/WorkspaceServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写失败测试**
|
|
|
+
|
|
|
+```java
|
|
|
+@Test
|
|
|
+void shouldReturnCurrentStageTasksAndAllowedActions() {
|
|
|
+ WorkspaceOverviewDto result = service.getOverview(periodId, employeeId);
|
|
|
+ assertEquals("OKR_ALIGN", result.getWorkflowState());
|
|
|
+ assertTrue(result.getTasks().stream().anyMatch(t -> "SUBMIT_OKR".equals(t.getAction())));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行并确认类不存在**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=WorkspaceServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现接口**
|
|
|
+
|
|
|
+```java
|
|
|
+@GetMapping("/overview")
|
|
|
+public ApiResult<WorkspaceOverviewDto> overview(@RequestParam(required = false) Long periodId) {
|
|
|
+ return ApiResult.success(service.getOverview(periodId, SecurityUtils.getCurrentUserId()));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+任务链接必须包含 `path`、`periodId`、`userId` 和业务对象 ID。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=WorkspaceServiceTest,AuthorizationServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/src/main/java/com/yimeng/okr/controller/WorkspaceController.java backend/src/main/java/com/yimeng/okr/service/WorkspaceService.java backend/src/main/java/com/yimeng/okr/dto/WorkspaceOverviewDto.java backend/src/main/java/com/yimeng/okr/dto/WorkflowActionDto.java backend/src/test/java/com/yimeng/okr/service/WorkspaceServiceTest.java
|
|
|
+git commit -m "feat(backend): 提供工作台聚合上下文"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 7:前端 API 分层与 workspace store
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`frontend/src/api/client.js`
|
|
|
+- 创建:`frontend/src/api/workspace.js`
|
|
|
+- 创建:`frontend/src/api/okr.js`
|
|
|
+- 创建:`frontend/src/api/scoring.js`
|
|
|
+- 修改:`frontend/src/api/index.js`
|
|
|
+- 创建:`frontend/src/stores/workspace.js`
|
|
|
+- 测试:`frontend/src/__tests__/workspace-store.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写 store 失败测试**
|
|
|
+
|
|
|
+```js
|
|
|
+it('uses URL periodId before remembered period', async () => {
|
|
|
+ const store = useWorkspaceStore()
|
|
|
+ await store.initialize({ routePeriodId: 9 })
|
|
|
+ expect(store.currentPeriod.id).toBe(9)
|
|
|
+})
|
|
|
+
|
|
|
+it('normalizes version conflicts without discarding details', async () => {
|
|
|
+ const error = normalizeApiError(conflictResponse)
|
|
|
+ expect(error).toMatchObject({ code: 409, conflictVersion: 4 })
|
|
|
+ expect(error.fieldErrors).toEqual(conflictResponse.data.fieldErrors)
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行并确认 store 不存在**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/workspace-store.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现选择顺序**
|
|
|
+
|
|
|
+```js
|
|
|
+const selectedId = routePeriodId
|
|
|
+ ?? Number(sessionStorage.getItem('workspacePeriodId'))
|
|
|
+ ?? overview.recommendedPeriodId
|
|
|
+```
|
|
|
+
|
|
|
+`client.js` 将 `code`、`message`、`fieldErrors`、`details` 和 `conflictVersion` 规范化为统一错误对象;HTTP 409 不被转换为通用 500。`index.js` 保留兼容导出,已有页面在后续任务逐步迁移。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/workspace-store.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/api/client.js frontend/src/api/workspace.js frontend/src/api/okr.js frontend/src/api/scoring.js frontend/src/api/index.js frontend/src/stores/workspace.js frontend/src/__tests__/workspace-store.test.js
|
|
|
+git commit -m "refactor(frontend): 统一 API 与周期工作区上下文"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 8:双层壳层与工作台路由
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`frontend/src/components/shell/AppShell.vue`
|
|
|
+- 创建:`frontend/src/components/shell/ModuleRail.vue`
|
|
|
+- 创建:`frontend/src/components/shell/ContextNav.vue`
|
|
|
+- 创建:`frontend/src/components/shell/SystemStatusBar.vue`
|
|
|
+- 创建:`frontend/src/components/common/PageContextBar.vue`
|
|
|
+- 修改:`frontend/src/components/MainLayout.vue`
|
|
|
+- 修改:`frontend/src/views/auth/LoginView.vue`
|
|
|
+- 修改:`frontend/src/router/index.js`
|
|
|
+- 测试:`frontend/src/__tests__/app-shell.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写导航测试**
|
|
|
+
|
|
|
+```js
|
|
|
+it('shows only modules allowed by the current user context', () => {
|
|
|
+ const wrapper = mount(ModuleRail, { props: { modules, allowedModules: ['workspace', 'okr'] } })
|
|
|
+ expect(wrapper.text()).toContain('工作台')
|
|
|
+ expect(wrapper.text()).not.toContain('系统管理')
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/app-shell.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现四个一级模块和上下文导航**
|
|
|
+
|
|
|
+`MainLayout.vue` 只负责挂载:
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <AppShell>
|
|
|
+ <router-view />
|
|
|
+ </AppShell>
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+新增路由:
|
|
|
+
|
|
|
+```js
|
|
|
+{ path: 'workspace', name: 'Workspace', component: () => import('../views/workspace/WorkspaceView.vue') }
|
|
|
+```
|
|
|
+
|
|
|
+根路由改为 `/workspace`。登录页使用同一品牌 token 和克制的临床工作区表面,但不挂载登录后的导航壳层。
|
|
|
+
|
|
|
+在组件测试中分别设置 1440、1100 和 900px 视口,验证完整上下文导航、弹出上下文导航和抽屉导航三种模式;表格窄屏只隐藏次要列,并保留行详情入口。
|
|
|
+
|
|
|
+- [ ] **步骤 4:补充壳层守卫并运行测试、守卫和构建**
|
|
|
+
|
|
|
+在 `frontend/scripts/verify-medtech-theme.mjs` 增加:
|
|
|
+
|
|
|
+```js
|
|
|
+if (!layout.includes('AppShell')) failures.push('MainLayout must render AppShell')
|
|
|
+if (!router.includes("path: 'workspace'")) failures.push('router must expose /workspace')
|
|
|
+```
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+node scripts/verify-medtech-theme.mjs
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/components/shell/AppShell.vue frontend/src/components/shell/ModuleRail.vue frontend/src/components/shell/ContextNav.vue frontend/src/components/shell/SystemStatusBar.vue frontend/src/components/common/PageContextBar.vue frontend/src/components/MainLayout.vue frontend/src/views/auth/LoginView.vue frontend/src/router/index.js frontend/src/__tests__/app-shell.test.js frontend/scripts/verify-medtech-theme.mjs
|
|
|
+git commit -m "feat(frontend): 实现手术观测台双层壳层"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 9:工作台首页
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`frontend/src/views/workspace/WorkspaceView.vue`
|
|
|
+- 创建:`frontend/src/components/common/WorkflowRail.vue`
|
|
|
+- 测试:`frontend/src/__tests__/workspace-view.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写待办深链接测试**
|
|
|
+
|
|
|
+```js
|
|
|
+it('opens the exact task target', async () => {
|
|
|
+ await wrapper.get('[data-task-id="review-12"]').trigger('click')
|
|
|
+ expect(router.currentRoute.value.fullPath).toBe('/okr/team?periodId=3&objectiveId=12')
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/workspace-view.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现阶段轨道、待办、团队指标、风险和节点**
|
|
|
+
|
|
|
+页面只消费 `workspace` store,不直接调用多个业务 API。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试和构建**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/workspace/WorkspaceView.vue frontend/src/components/common/WorkflowRail.vue frontend/src/__tests__/workspace-view.test.js
|
|
|
+git commit -m "feat(frontend): 增加当前周期工作台"
|
|
|
+```
|
|
|
+
|
|
|
+## 阶段 2:OKR 链路
|
|
|
+
|
|
|
+### 任务 10:OKR 聚合查询、草稿和乐观锁
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/OkrWorkbenchDto.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/request/SaveOkrDraftRequest.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/request/ReviewOkrRequest.java`
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/request/UpdateKrProgressRequest.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/controller/OkrController.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/service/OkrService.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/service/OkrServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写草稿和冲突测试**
|
|
|
+
|
|
|
+```java
|
|
|
+assertEquals("DRAFT", service.saveDraft(request, userId).getStatus());
|
|
|
+BusinessException conflict = assertThrows(BusinessException.class,
|
|
|
+ () -> service.saveDraft(requestWithOldVersion, userId));
|
|
|
+assertEquals(409, conflict.getCode());
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=OkrServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现接口**
|
|
|
+
|
|
|
+```java
|
|
|
+@GetMapping("/workbench")
|
|
|
+ApiResult<OkrWorkbenchDto> workbench(@RequestParam Long periodId)
|
|
|
+
|
|
|
+@PutMapping("/{id}/draft")
|
|
|
+ApiResult<OkrDetailDto> saveDraft(@PathVariable Long id, @Valid @RequestBody SaveOkrDraftRequest request)
|
|
|
+```
|
|
|
+
|
|
|
+更新条件必须包含 `id` 和 `version`,成功后版本加 1。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行 OKR 和周期状态机测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=OkrServiceTest,AssessmentPeriodServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/src/main/java/com/yimeng/okr/controller/OkrController.java backend/src/main/java/com/yimeng/okr/service/OkrService.java backend/src/main/java/com/yimeng/okr/dto/OkrWorkbenchDto.java backend/src/main/java/com/yimeng/okr/dto/request/SaveOkrDraftRequest.java backend/src/main/java/com/yimeng/okr/dto/request/ReviewOkrRequest.java backend/src/main/java/com/yimeng/okr/dto/request/UpdateKrProgressRequest.java backend/src/test/java/com/yimeng/okr/service/OkrServiceTest.java
|
|
|
+git commit -m "feat(backend): 增加 OKR 工作台草稿与版本控制"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 11:重构我的 OKR
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/okr/MyOkrView.vue`
|
|
|
+- 创建:`frontend/src/components/workbench/SplitWorkbench.vue`
|
|
|
+- 创建:`frontend/src/components/workbench/InspectorPanel.vue`
|
|
|
+- 测试:`frontend/src/__tests__/my-okr-workbench.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写提交检查测试**
|
|
|
+
|
|
|
+```js
|
|
|
+expect(validateOkr({ krs: [{ weight: 60 }, { weight: 30 }] }))
|
|
|
+ .toContain('KR 权重合计必须为 100')
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/my-okr-workbench.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现上级目标、编辑区和提交检查器**
|
|
|
+
|
|
|
+保存草稿传 `version`;409 时展示冲突状态,不覆盖用户输入。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试、守卫和构建**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+node scripts/verify-medtech-theme.mjs
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/okr/MyOkrView.vue frontend/src/components/workbench/SplitWorkbench.vue frontend/src/components/workbench/InspectorPanel.vue frontend/src/__tests__/my-okr-workbench.test.js
|
|
|
+git commit -m "feat(frontend): 重构个人 OKR 制定工作台"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 12:团队审核、对齐树和执行进度
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/okr/TeamOkrView.vue`
|
|
|
+- 修改:`frontend/src/views/okr/AlignmentView.vue`
|
|
|
+- 创建:`frontend/src/views/okr/OkrExecutionView.vue`
|
|
|
+- 修改:`frontend/src/router/index.js`
|
|
|
+- 测试:`frontend/src/__tests__/okr-review-flow.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写驳回原因测试**
|
|
|
+
|
|
|
+```js
|
|
|
+await wrapper.get('[data-action="reject"]').trigger('click')
|
|
|
+expect(wrapper.get('[data-action="confirm-reject"]').attributes('disabled')).toBeDefined()
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/okr-review-flow.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现三页**
|
|
|
+
|
|
|
+增加 `/okr/execution`;团队页按待审核、未提交、驳回、阻塞排序;对齐页使用树形矩阵;执行页复用 KR 详情抽屉。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行前后端 OKR 测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd frontend && npm test && npm run build
|
|
|
+cd ../backend && mvn -Dtest=OkrServiceTest,AssessmentPeriodServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/okr/TeamOkrView.vue frontend/src/views/okr/AlignmentView.vue frontend/src/views/okr/OkrExecutionView.vue frontend/src/router/index.js frontend/src/__tests__/okr-review-flow.test.js
|
|
|
+git commit -m "feat(frontend): 打通团队审核对齐与执行进度"
|
|
|
+```
|
|
|
+
|
|
|
+## 阶段 3:绩效链路
|
|
|
+
|
|
|
+### 任务 13:评分 DTO、评审队列与历史接口
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`backend/src/main/java/com/yimeng/okr/dto/request/SubmitScoreRequest.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/dto/ScoringDto.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/controller/PerformanceController.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/service/PerformanceService.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/integration/PerformanceLifecycleApiIntegrationTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写接口契约测试**
|
|
|
+
|
|
|
+```java
|
|
|
+mockMvc.perform(get("/api/scores/history"))
|
|
|
+ .andExpect(status().isOk())
|
|
|
+ .andExpect(jsonPath("$.data[0].periodName").exists());
|
|
|
+```
|
|
|
+
|
|
|
+同时断言评审详情包含 `workflowState`、`allowedActions`、`items` 和 `version`。
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=PerformanceLifecycleApiIntegrationTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现**
|
|
|
+
|
|
|
+新增:
|
|
|
+
|
|
|
+```java
|
|
|
+@GetMapping("/review-queue")
|
|
|
+ApiResult<List<ScoringDto>> reviewQueue(@RequestParam Long periodId)
|
|
|
+
|
|
|
+@GetMapping("/history")
|
|
|
+ApiResult<List<ScoringDto>> history()
|
|
|
+```
|
|
|
+
|
|
|
+提交自评和终评改用 `@Valid SubmitScoreRequest`,旧 JSON 同步保存。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行评分测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=PerformanceServiceTest,PerformanceLifecycleIntegrationTest,PerformanceLifecycleApiIntegrationTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/src/main/java/com/yimeng/okr/controller/PerformanceController.java backend/src/main/java/com/yimeng/okr/service/PerformanceService.java backend/src/main/java/com/yimeng/okr/dto/ScoringDto.java backend/src/main/java/com/yimeng/okr/dto/request/SubmitScoreRequest.java backend/src/test/java/com/yimeng/okr/integration/PerformanceLifecycleApiIntegrationTest.java
|
|
|
+git commit -m "feat(backend): 提供结构化评审队列和绩效历史"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 14:评审中心
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/scoring/ScoringListView.vue`
|
|
|
+- 修改:`frontend/src/views/scoring/ScoringEditView.vue`
|
|
|
+- 创建:`frontend/src/components/workbench/StickyActionBar.vue`
|
|
|
+- 测试:`frontend/src/__tests__/scoring-workbench.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写差异诊断测试**
|
|
|
+
|
|
|
+```js
|
|
|
+expect(buildDiagnostics({ selfTotal: 88, finalTotal: 72, missingEvidence: 1 }))
|
|
|
+ .toEqual(expect.arrayContaining([
|
|
|
+ expect.objectContaining({ code: 'SCORE_DIFF' }),
|
|
|
+ expect.objectContaining({ code: 'MISSING_EVIDENCE' })
|
|
|
+ ]))
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/scoring-workbench.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现队列、三列矩阵、检查器和底部栏**
|
|
|
+
|
|
|
+评分提交必须使用结构化 items;页面不再解析 `commentsJson` 作为首选数据源。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行评分契约和测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+node scripts/verify-scoring-contract.mjs
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/scoring/ScoringListView.vue frontend/src/views/scoring/ScoringEditView.vue frontend/src/components/workbench/StickyActionBar.vue frontend/src/__tests__/scoring-workbench.test.js
|
|
|
+git commit -m "feat(frontend): 重构绩效评审中心"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 15:我的绩效与历史
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/scoring/MyResultView.vue`
|
|
|
+- 修改:`frontend/src/views/scoring/HistoryView.vue`
|
|
|
+- 测试:`frontend/src/__tests__/my-performance-flow.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写阶段主操作测试**
|
|
|
+
|
|
|
+```js
|
|
|
+expect(primaryAction({ workflowState: 'SELF_REQUIRED' })).toBe('SUBMIT_SELF')
|
|
|
+expect(primaryAction({ workflowState: 'RESULT_PUBLISHED' })).toBe('CONFIRM_OR_APPEAL')
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/my-performance-flow.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:统一自评、结果、面谈和确认状态**
|
|
|
+
|
|
|
+历史页改用 `/api/scores/history`,删除前端逐周期轮询。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试、契约和构建**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+node scripts/verify-scoring-contract.mjs
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/scoring/MyResultView.vue frontend/src/views/scoring/HistoryView.vue frontend/src/__tests__/my-performance-flow.test.js
|
|
|
+git commit -m "feat(frontend): 统一我的绩效与历史流程"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 16:绩效沟通、申诉和审计修正
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/feedback/FeedbackView.vue`
|
|
|
+- 修改:`frontend/src/views/scoring/ScoreAppealView.vue`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/service/FeedbackService.java`
|
|
|
+- 修改:`backend/src/main/java/com/yimeng/okr/service/ScoreAppealService.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/service/FeedbackServiceTest.java`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/service/ScoreAppealServiceTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写关联终评分和修正审计测试**
|
|
|
+
|
|
|
+```java
|
|
|
+assertEquals(finalScoreId, service.getFeedbackPage(periodId, userId, superiorId)
|
|
|
+ .getFinalScore().getId());
|
|
|
+assertEquals(1, auditCorrectionMapper.selectCount(
|
|
|
+ Wrappers.<AuditCorrection>lambdaQuery().eq(AuditCorrection::getPerformanceId, finalScoreId)));
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+mvn -Dtest=FeedbackServiceTest,ScoreAppealServiceTest test
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现列表、详情和受控操作**
|
|
|
+
|
|
|
+面谈关闭前必须存在员工回复;申诉修正必须包含非空原因并写审计记录。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行前后端测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && mvn -Dtest=FeedbackServiceTest,ScoreAppealServiceTest,PerformanceServiceTest test
|
|
|
+cd ../frontend && npm test && npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/feedback/FeedbackView.vue frontend/src/views/scoring/ScoreAppealView.vue backend/src/main/java/com/yimeng/okr/service/FeedbackService.java backend/src/main/java/com/yimeng/okr/service/ScoreAppealService.java backend/src/test/java/com/yimeng/okr/service/FeedbackServiceTest.java backend/src/test/java/com/yimeng/okr/service/ScoreAppealServiceTest.java
|
|
|
+git commit -m "feat: 打通绩效沟通申诉与审计修正"
|
|
|
+```
|
|
|
+
|
|
|
+## 阶段 4:管理页面统一
|
|
|
+
|
|
|
+### 任务 17:周期、组织和配置页面
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/period/PeriodListView.vue`
|
|
|
+- 修改:`frontend/src/views/org/OrgView.vue`
|
|
|
+- 修改:`frontend/src/views/period/KpiTemplateView.vue`
|
|
|
+- 修改:`frontend/src/views/system/DimensionConfigView.vue`
|
|
|
+- 测试:`frontend/src/__tests__/admin-workbenches.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写危险操作影响范围测试**
|
|
|
+
|
|
|
+```js
|
|
|
+expect(transitionImpact({ target: 'ARCHIVED', participantCount: 42 }))
|
|
|
+ .toContain('42 名参与人')
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/admin-workbenches.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:统一为数据浏览或分栏工作台**
|
|
|
+
|
|
|
+周期流转确认框展示阻塞项和影响人数;组织页使用树、人员矩阵和检查器;模板与维度页使用统一编辑表格。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试和构建**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/period/PeriodListView.vue frontend/src/views/period/KpiTemplateView.vue frontend/src/views/org/OrgView.vue frontend/src/views/system/DimensionConfigView.vue frontend/src/__tests__/admin-workbenches.test.js
|
|
|
+git commit -m "feat(frontend): 统一周期组织与绩效配置工作台"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 18:通知和操作日志
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/src/views/notification/NotificationView.vue`
|
|
|
+- 修改:`frontend/src/views/system/LogView.vue`
|
|
|
+- 修改:`frontend/src/components/shell/SystemStatusBar.vue`
|
|
|
+- 测试:`frontend/src/__tests__/notification-routing.test.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写通知深链接测试**
|
|
|
+
|
|
|
+```js
|
|
|
+expect(resolveNotificationTarget({
|
|
|
+ refType: 'objective', refId: 12, periodId: 3
|
|
|
+})).toBe('/okr/my?periodId=3&objectiveId=12')
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx vitest run src/__tests__/notification-routing.test.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:实现通知中心和日志数据表**
|
|
|
+
|
|
|
+通知点击直接进入业务对象;日志支持操作类型、人员、时间筛选和详情抽屉。
|
|
|
+
|
|
|
+- [ ] **步骤 4:运行测试和主题守卫**
|
|
|
+
|
|
|
+```bash
|
|
|
+npm test
|
|
|
+node scripts/verify-medtech-theme.mjs
|
|
|
+npm run build
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/src/views/notification/NotificationView.vue frontend/src/views/system/LogView.vue frontend/src/components/shell/SystemStatusBar.vue frontend/src/__tests__/notification-routing.test.js
|
|
|
+git commit -m "feat(frontend): 统一通知与操作日志体验"
|
|
|
+```
|
|
|
+
|
|
|
+## 阶段 5:全链路验收与部署
|
|
|
+
|
|
|
+### 任务 19:Playwright 关键链路
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`frontend/package.json`
|
|
|
+- 创建:`frontend/playwright.config.js`
|
|
|
+- 创建:`frontend/e2e/okr-lifecycle.spec.js`
|
|
|
+- 创建:`frontend/e2e/performance-lifecycle.spec.js`
|
|
|
+- 创建:`backend/src/test/java/com/yimeng/okr/integration/E2eSeedIntegrationTest.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:安装 Playwright 并编写失败测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd frontend
|
|
|
+npm install -D @playwright/test
|
|
|
+npx playwright install chromium
|
|
|
+```
|
|
|
+
|
|
|
+```js
|
|
|
+test('employee completes OKR lifecycle', async ({ page }) => {
|
|
|
+ await page.goto('/#/workspace')
|
|
|
+ await page.getByText('制定 OKR').click()
|
|
|
+ await expect(page.getByText('KR 权重合计')).toBeVisible()
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行并确认测试数据或流程缺失**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx playwright test e2e/okr-lifecycle.spec.js
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:增加固定 E2E 数据集**
|
|
|
+
|
|
|
+数据集包含管理员、经理、员工、一个 `OKR_ALIGN` 周期和一个 `ASSESSING` 周期,仅在 `e2e` profile 初始化。
|
|
|
+
|
|
|
+- [ ] **步骤 4:覆盖两条完整链路**
|
|
|
+
|
|
|
+OKR 测试覆盖制定、驳回、修改、通过和进度更新;绩效测试覆盖自评、终评、发布、面谈、确认、申诉和修正。
|
|
|
+
|
|
|
+- [ ] **步骤 5:运行 E2E**
|
|
|
+
|
|
|
+```bash
|
|
|
+npx playwright test
|
|
|
+```
|
|
|
+
|
|
|
+预期:全部通过。
|
|
|
+
|
|
|
+- [ ] **步骤 6:提交**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add frontend/package.json frontend/package-lock.json frontend/playwright.config.js frontend/e2e/okr-lifecycle.spec.js frontend/e2e/performance-lifecycle.spec.js backend/src/test/java/com/yimeng/okr/integration/E2eSeedIntegrationTest.java
|
|
|
+git commit -m "test: 覆盖 OKR 绩效关键链路"
|
|
|
+```
|
|
|
+
|
|
|
+### 任务 20:历史数据库和单 JAR 验收
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`scripts/build.sh`
|
|
|
+- 修改:`frontend/scripts/verify-medtech-theme.mjs`
|
|
|
+- 测试:`backend/src/test/java/com/yimeng/okr/config/DatabaseMigrationRunnerTest.java`
|
|
|
+- 构建产物:`backend/src/main/resources/static/`
|
|
|
+
|
|
|
+- [ ] **步骤 1:复制生产数据结构的脱敏数据库用于迁移测试**
|
|
|
+
|
|
|
+测试只操作临时副本,不修改 `data/okr-performance.db`。
|
|
|
+
|
|
|
+- [ ] **步骤 2:运行全部后端测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend
|
|
|
+mvn test
|
|
|
+```
|
|
|
+
|
|
|
+预期:0 failures。
|
|
|
+
|
|
|
+- [ ] **步骤 3:运行全部前端验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd frontend
|
|
|
+npm test
|
|
|
+node scripts/verify-medtech-theme.mjs
|
|
|
+node scripts/verify-scoring-contract.mjs
|
|
|
+npm run build
|
|
|
+npx playwright test
|
|
|
+```
|
|
|
+
|
|
|
+预期:全部通过。
|
|
|
+
|
|
|
+- [ ] **步骤 4:让构建脚本同步静态资源并校验一致**
|
|
|
+
|
|
|
+`scripts/build.sh` 在复制前完成前端测试、两个契约守卫和生产构建;复制到临时目录并通过 `diff -qr` 后,再替换 `backend/src/main/resources/static/`,避免失败构建留下半成品。
|
|
|
+
|
|
|
+```bash
|
|
|
+cd ..
|
|
|
+./scripts/build.sh
|
|
|
+diff -qr frontend/dist backend/src/main/resources/static
|
|
|
+```
|
|
|
+
|
|
|
+预期:`diff` 无输出。
|
|
|
+
|
|
|
+- [ ] **步骤 5:构建单 JAR**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend
|
|
|
+mvn clean package
|
|
|
+java -jar target/okr-performance-1.0.0.jar
|
|
|
+```
|
|
|
+
|
|
|
+验证:
|
|
|
+
|
|
|
+- 新数据库可初始化。
|
|
|
+- 历史数据库副本会先备份再迁移。
|
|
|
+- 登录、工作台、OKR、绩效、管理页可访问。
|
|
|
+- 浏览器控制台无错误。
|
|
|
+
|
|
|
+- [ ] **步骤 6:提交构建产物**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/src/main/resources/static scripts/build.sh frontend/scripts/verify-medtech-theme.mjs
|
|
|
+git commit -m "build: 完成手术观测台单 JAR 交付"
|
|
|
+```
|
|
|
+
|
|
|
+## 最终验收清单
|
|
|
+
|
|
|
+- [ ] 全部页面使用双层壳层和统一 token。
|
|
|
+- [ ] 全局周期上下文与 URL 同步。
|
|
|
+- [ ] 工作台待办可以直达具体对象。
|
|
|
+- [ ] OKR 草稿、提交、驳回、审核和执行进度闭环通过。
|
|
|
+- [ ] 自评、终评、发布、面谈、确认和申诉闭环通过。
|
|
|
+- [ ] 评分明细已结构化,旧 JSON 保留。
|
|
|
+- [ ] 迁移前自动备份,失败事务回滚。
|
|
|
+- [ ] 前后端评分口径守卫通过。
|
|
|
+- [ ] 历史绩效不再使用 N+1 查询。
|
|
|
+- [ ] 前端测试、后端测试和 Playwright 全部通过。
|
|
|
+- [ ] 单 JAR 构建和历史数据库启动通过。
|