From e532d4d9153dbc449674beb66e3524d58385491c Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 27 Feb 2026 17:19:58 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=BB=9F=E8=AE=A1=E4=BF=AE=E5=A4=8D=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-02-27-project-status-counts-fix-design.md | 635 ++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 docs/plans/2026-02-27-project-status-counts-fix-design.md diff --git a/docs/plans/2026-02-27-project-status-counts-fix-design.md b/docs/plans/2026-02-27-project-status-counts-fix-design.md new file mode 100644 index 0000000..9083add --- /dev/null +++ b/docs/plans/2026-02-27-project-status-counts-fix-design.md @@ -0,0 +1,635 @@ +# 项目管理标签页状态统计修复设计文档 + +## 文档信息 + +- **创建日期**: 2026-02-27 +- **作者**: Claude Code +- **状态**: 待实施 + +## 问题背景 + +### 当前问题 + +项目管理页面的标签页筛选功能中,各状态的项目计数不正确。具体表现为: + +- **现象**: 标签页显示的数量远小于实际总数 +- **根本原因**: 前端只统计当前页的数据来计算各状态的数量 +- **影响**: 用户无法了解各状态的完整项目分布情况 + +### 代码位置 + +**前端代码**: +- 页面组件: `ruoyi-ui/src/views/ccdiProject/index.vue` +- API 定义: `ruoyi-ui/src/api/ccdiProject.js` + +**后端代码**: +- Controller: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java` +- Service 接口: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java` +- Service 实现: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java` + +### 当前实现分析 + +**问题代码** (index.vue:136-145): + +```javascript +calculateTabCounts() { + // 注意:这里需要后端API返回所有状态的数量统计 + // 目前暂时使用当前页的数据进行计算 + this.tabCounts = { + all: this.total, + '0': this.projectList.filter(p => p.status === '0').length, + '1': this.projectList.filter(p => p.status === '1').length, + '2': this.projectList.filter(p => p.status === '2').length + } +} +``` + +**问题分析**: +- `projectList` 只包含当前页的数据(默认10条) +- 过滤计算只能得到当前页中各状态的数量 +- 用户看到的是"当前页有5个进行中的项目",而非"总共有30个进行中的项目" + +## 需求说明 + +### 功能需求 + +**预期行为**: +- 标签页应显示数据库中所有该状态的项目总数 +- 状态统计不受搜索条件和分页影响 +- 状态统计随列表一起刷新 + +**非功能性需求**: +- 性能: 统计查询应快速响应(< 100ms) +- 准确性: 统计数字必须与数据库一致 +- 实时性: 每次刷新列表时同步更新统计 + +## 设计方案 + +### 整体架构 + +采用**独立统计接口**方案: +- 后端新增 `/ccdi/project/statusCounts` 接口 +- 前端在加载列表时并行调用统计接口 +- 两个请求独立,数据互不影响 + +### 后端设计 + +#### 1. 新增 VO 类 + +**文件**: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java` + +```java +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +/** + * 项目状态统计VO + */ +@Data +public class CcdiProjectStatusCountsVO { + /** 全部项目总数 */ + private Long all; + /** 进行中项目数(状态0) */ + private Long status0; + /** 已完成项目数(状态1) */ + private Long status1; + /** 已归档项目数(状态2) */ + private Long status2; +} +``` + +**设计说明**: +- 使用 `Long` 类型支持大数据量 +- 字段命名清晰,便于前端使用 +- 不包含搜索条件,始终统计全量数据 + +#### 2. Service 层 + +**接口定义** (`ICcdiProjectService.java`): + +```java +/** + * 查询各状态的项目总数(不受搜索条件影响) + * @return 状态统计 + */ +CcdiProjectStatusCountsVO getStatusCounts(); +``` + +**实现类** (`CcdiProjectServiceImpl.java`): + +```java +@Override +public CcdiProjectStatusCountsVO getStatusCounts() { + CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO(); + + // 统计全部项目 + vo.setAll(ccdiProjectMapper.selectCount(null)); + + // 统计各状态项目 + vo.setStatus0(ccdiProjectMapper.selectCount( + new LambdaQueryWrapper() + .eq(CcdiProject::getStatus, "0") + )); + vo.setStatus1(ccdiProjectMapper.selectCount( + new LambdaQueryWrapper() + .eq(CcdiProject::getStatus, "1") + )); + vo.setStatus2(ccdiProjectMapper.selectCount( + new LambdaQueryWrapper() + .eq(CcdiProject::getStatus, "2") + )); + + return vo; +} +``` + +**实现要点**: +- 使用 MyBatis Plus 的 `selectCount` 方法 +- 不添加任何过滤条件 +- 可以优化为一次查询返回所有统计(使用 GROUP BY) + +**优化方案**(可选): + +如果性能要求高,可以改为单次查询: + +```java +@Override +public CcdiProjectStatusCountsVO getStatusCounts() { + // 查询各状态的统计 + List> counts = ccdiProjectMapper.countGroupByStatus(); + + CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO(); + vo.setAll(0L); + vo.setStatus0(0L); + vo.setStatus1(0L); + vo.setStatus2(0L); + + for (Map item : counts) { + String status = (String) item.get("status"); + Long count = (Long) item.get("count"); + + vo.setAll(vo.getAll() + count); + if ("0".equals(status)) { + vo.setStatus0(count); + } else if ("1".equals(status)) { + vo.setStatus1(count); + } else if ("2".equals(status)) { + vo.setStatus2(count); + } + } + + return vo; +} +``` + +Mapper 方法: + +```java +List> countGroupByStatus(); +``` + +XML SQL: + +```xml + +``` + +#### 3. Controller 层 + +**文件**: `CcdiProjectController.java` + +```java +/** + * 查询项目状态统计 + */ +@GetMapping("/statusCounts") +@Operation(summary = "查询项目状态统计") +@PreAuthorize("@ss.hasPermi('ccdi:project:list')") +public AjaxResult getStatusCounts() { + CcdiProjectStatusCountsVO counts = projectService.getStatusCounts(); + return AjaxResult.success(counts); +} +``` + +**设计说明**: +- 不接收任何查询参数 +- 使用与列表接口相同的权限注解 +- 返回标准的 AjaxResult 格式 + +### 前端设计 + +#### 1. API 层 + +**文件**: `ruoyi-ui/src/api/ccdiProject.js` + +```javascript +// 查询项目状态统计 +export function getStatusCounts() { + return request({ + url: '/ccdi/project/statusCounts', + method: 'get' + }) +} +``` + +#### 2. 页面组件改造 + +**文件**: `ruoyi-ui/src/views/ccdiProject/index.vue` + +**修改点 1**: 导入新 API + +```javascript +import { listProject, getStatusCounts } from '@/api/ccdiProject' +``` + +**修改点 2**: 重构 `getList()` 方法 + +```javascript +/** 查询项目列表 */ +getList() { + this.loading = true + + // 并行请求列表数据和状态统计 + Promise.all([ + listProject(this.queryParams), + getStatusCounts() + ]).then(([listResponse, countsResponse]) => { + // 处理列表数据 + this.projectList = listResponse.rows + this.total = listResponse.total + + // 处理状态统计 + const counts = countsResponse.data + this.tabCounts = { + all: counts.all, + '0': counts.status0, + '1': counts.status1, + '2': counts.status2 + } + + this.loading = false + }).catch(() => { + this.loading = false + }) +} +``` + +**修改点 3**: 删除旧方法 + +删除 `calculateTabCounts()` 方法及其调用(第 136-145 行)。 + +**设计说明**: +- 使用 `Promise.all` 并行请求,提高性能 +- 两个请求独立失败互不影响(可以优化错误处理) +- 状态统计使用后端返回的准确数据 + +### 数据流设计 + +``` +用户操作(打开页面/搜索/切换标签/翻页) + ↓ +前端调用 getList() 方法 + ↓ +并行发送两个请求: + ┌─────────────────────────┬────────────────────────┐ + │ listProject(queryParams)│ getStatusCounts() │ + │ - 带搜索条件 │ - 无参数 │ + │ - 带分页参数 │ - 返回全量统计 │ + │ - 返回当前页数据 │ │ + └─────────────────────────┴────────────────────────┘ + ↓ ↓ +后端处理 后端处理 +- 根据条件过滤 - COUNT 查询全部 +- 分页返回结果 - 按 status 分组统计 + ↓ ↓ +前端接收响应 ←─────────────────┘ + ↓ +更新状态: +- projectList: 列表数据(受搜索/分页影响) +- total: 当前筛选条件的总数 +- tabCounts: 各状态的完整统计(不受搜索影响) + ↓ +页面渲染: +- 表格: 显示当前页项目 +- 标签: 显示固定统计数字 +``` + +**关键特性**: +1. **并行请求**: 两个请求同时发出,不阻塞 +2. **数据独立**: 列表数据和统计数据来源不同 +3. **统计固定**: 标签页数字不随搜索/分页变化 +4. **列表过滤**: 表格内容根据搜索条件正确过滤 + +## 测试方案 + +### 后端测试 + +#### 1. 单元测试 + +**测试文件**: `CcdiProjectServiceTest.java` + +```java +@Test +void testGetStatusCounts() { + // 准备测试数据 + // 创建 3 个进行中项目 + // 创建 2 个已完成项目 + // 创建 1 个已归档项目 + + // 执行测试 + CcdiProjectStatusCountsVO result = projectService.getStatusCounts(); + + // 验证结果 + assertEquals(6L, result.getAll()); + assertEquals(3L, result.getStatus0()); + assertEquals(2L, result.getStatus1()); + assertEquals(1L, result.getStatus2()); +} +``` + +#### 2. 接口测试 + +**使用 Swagger UI 测试**: + +1. 访问: `http://localhost:8080/swagger-ui/index.html` +2. 找到: 纪检初核项目管理 → GET /ccdi/project/statusCounts +3. 点击 "Try it out" → "Execute" + +**预期响应**: + +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "all": 100, + "status0": 30, + "status1": 50, + "status2": 20 + } +} +``` + +**测试用例**: + +| 用例编号 | 场景 | 预期结果 | +|---------|------|---------| +| TC01 | 数据库无项目 | all=0, status0=0, status1=0, status2=0 | +| TC02 | 只有进行中项目 | all=N, status0=N, status1=0, status2=0 | +| TC03 | 混合状态项目 | 各状态数字之和等于 all | +| TC04 | 大数据量(1000+) | 查询时间 < 100ms | + +### 前端测试 + +#### 1. 页面加载测试 + +**步骤**: +1. 打开项目管理页面 +2. 观察标签页数字 + +**预期结果**: +- 标签页显示正确的总数(例如:全部项目(100)) +- 数字不随分页变化 + +#### 2. 搜索测试 + +**步骤**: +1. 在搜索框输入项目名称 +2. 点击搜索 + +**预期结果**: +- 列表正确过滤 +- 标签页数字保持不变(显示总数) + +#### 3. 分页测试 + +**步骤**: +1. 切换到第 2 页 + +**预期结果**: +- 列表切换到第 2 页 +- 标签页数字保持不变 + +#### 4. 状态切换测试 + +**步骤**: +1. 点击"进行中"标签 + +**预期结果**: +- 列表只显示进行中的项目 +- 标签页数字保持不变(仍显示总数) + +#### 5. 网络错误测试 + +**步骤**: +1. 模拟统计接口失败 + +**预期结果**: +- 列表正常显示 +- 标签页显示 0 或保持上次数据 +- 不阻塞用户操作 + +### 集成测试 + +**端到端测试流程**: + +1. **准备数据**: 在数据库中插入测试项目 + - 10 个进行中项目 + - 15 个已完成项目 + - 5 个已归档项目 + +2. **执行测试**: + ``` + a. 打开项目管理页面 + b. 验证标签显示: 全部(30), 进行中(10), 已完成(15), 已归档(5) + c. 搜索项目名称(匹配 5 个) + d. 验证列表显示 5 个项目 + e. 验证标签仍显示: 全部(30), 进行中(10), 已完成(15), 已归档(5) + f. 切换到第 2 页 + g. 验证列表切换,标签数字不变 + h. 点击"进行中"标签 + i. 验证列表只显示进行中项目 + j. 验证标签仍显示: 全部(30), 进行中(10), 已完成(15), 已归档(5) + ``` + +3. **预期结果**: 所有验证点通过 + +## 性能考虑 + +### 后端性能 + +**统计查询优化**: +- 使用 `COUNT(*)` 查询,性能较好 +- 考虑为 `status` 字段添加索引(如果项目数量 > 10000) +- 单次 GROUP BY 查询优于多次 COUNT 查询 + +**预估性能**: +- 项目数 < 1000: 响应时间 < 50ms +- 项目数 1000-10000: 响应时间 50-100ms +- 项目数 > 10000: 考虑添加缓存或索引 + +### 前端性能 + +**并行请求**: +- `Promise.all` 同时发起两个请求 +- 总等待时间 = max(listTime, countsTime) +- 不影响用户体验 + +**请求频率**: +- 仅在页面加载、搜索、翻页时请求 +- 不会产生过多网络流量 + +## 风险与应对 + +### 风险 1: 统计数据不一致 + +**场景**: 用户在查看页面时,后台数据被其他用户修改 + +**影响**: 标签页数字与实际不符 + +**应对方案**: +- 轻微问题,可接受 +- 用户刷新页面后同步 +- 不需要额外处理 + +### 风险 2: 统计接口失败 + +**场景**: 统计接口异常或超时 + +**影响**: 标签页显示 0 或空白 + +**应对方案**: +- 前端增加错误处理 +- 列表接口不受影响 +- 控制台记录错误日志 + +### 风险 3: 大数据量性能 + +**场景**: 项目数量超过 10000 + +**影响**: 统计查询变慢 + +**应对方案**: +- 为 status 字段添加索引 +- 使用单次 GROUP BY 查询 +- 考虑添加缓存(Redis) + +## 实施计划 + +### 后端开发 (30 分钟) + +1. 创建 VO 类 (5 分钟) + - 新建 `CcdiProjectStatusCountsVO.java` + - 添加字段和 Lombok 注解 + +2. Service 层开发 (15 分钟) + - 接口添加方法声明 + - 实现类编写统计逻辑 + - (可选) Mapper 添加 GROUP BY 查询 + +3. Controller 层开发 (5 分钟) + - 添加 `/statusCounts` 接口 + - 添加 Swagger 注释 + +4. 本地测试 (5 分钟) + - 启动项目 + - 使用 Swagger 测试接口 + +### 前端开发 (20 分钟) + +1. API 层修改 (5 分钟) + - 添加 `getStatusCounts()` 函数 + +2. 页面组件修改 (10 分钟) + - 导入新 API + - 修改 `getList()` 方法 + - 删除 `calculateTabCounts()` 方法 + +3. 本地测试 (5 分钟) + - 启动前端 + - 验证功能 + +### 测试验证 (15 分钟) + +1. 后端接口测试 (5 分钟) + - Swagger 测试各场景 + +2. 前端功能测试 (10 分钟) + - 执行测试方案中的所有用例 + +### 总计时间: 65 分钟 + +## 验收标准 + +### 功能验收 + +- [ ] 后端 `/statusCounts` 接口返回正确的统计数字 +- [ ] 前端标签页显示数据库中的完整统计 +- [ ] 搜索不影响标签页统计数字 +- [ ] 分页不影响标签页统计数字 +- [ ] 状态过滤不影响标签页统计数字 + +### 性能验收 + +- [ ] 统计接口响应时间 < 100ms +- [ ] 页面加载时间无明显增加 + +### 代码质量 + +- [ ] 后端代码符合项目规范 +- [ ] 前端代码符合项目规范 +- [ ] 添加必要的注释和文档 + +## 后续优化 + +### 短期优化 (可选) + +1. **错误处理增强** + - 前端对统计接口失败进行友好提示 + - 后端添加异常捕获和日志 + +2. **性能优化** + - 使用单次 GROUP BY 查询替代多次 COUNT + - 为 status 字段添加索引 + +### 长期优化 (可选) + +1. **缓存机制** + - 使用 Redis 缓存统计结果 + - 设置 5 分钟过期时间 + - 项目变更时清除缓存 + +2. **实时更新** + - 使用 WebSocket 推送统计更新 + - 减少轮询请求 + +## 附录 + +### 相关文件清单 + +**后端新增文件**: +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java` + +**后端修改文件**: +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java` +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java` (可选) +- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml` (可选) + +**前端修改文件**: +- `ruoyi-ui/src/api/ccdiProject.js` +- `ruoyi-ui/src/views/ccdiProject/index.vue` + +### 参考资料 + +- MyBatis Plus 官方文档: https://baomidou.com/ +- Element UI 标签页组件: https://element.eleme.cn/#/zh-CN/component/tabs +- Promise.all 文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all