# 项目管理标签页状态统计修复设计文档 ## 文档信息 - **创建日期**: 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 分钟 ## 验收标准 ### 功能验收 - [x] 后端 `/statusCounts` 接口返回正确的统计数字 - [x] 前端标签页显示数据库中的完整统计 - [x] 搜索不影响标签页统计数字 - [x] 分页不影响标签页统计数字 - [x] 状态过滤不影响标签页统计数字 ### 性能验收 - [x] 统计接口响应时间 < 100ms - [x] 页面加载时间无明显增加 ### 代码质量 - [x] 后端代码符合项目规范 - [x] 前端代码符合项目规范 - [x] 添加必要的注释和文档 ## 后续优化 ### 短期优化 (可选) 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