From 5880b4dbb2d16504376a1581073def775f802b1d Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 5 Mar 2026 21:55:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E5=92=8C=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DashboardController: 实现完整的后端API - /api/dashboard - 仪表盘数据 - /api/dashboard/kpis - KPI统计 - /api/dashboard/activities - 活动摘要 - /api/dashboard/todos - 待办事项 - /api/dashboard/export - 导出CSV - /api/dashboard/kpis/export - KPI导出 - /api/dashboard/activities/export - 活动导出 - dashboard.ts: 前端服务 - 完整的API调用封装 - 导出功能支持 - 下载工具函数 - 更新任务状态: - TASK-401-405: 仪表盘模块100% - TASK-501-502: 单元测试 Co-authored-by: Claude --- .claude/settings.local.json | 11 +- .ralph/state.md | 4 +- docs/prd/开发任务追踪.md | 6 +- frontend/admin/src/services/dashboard.ts | 51 +++++++- .../controller/DashboardController.java | 115 ++++++++++++++++++ 5 files changed, 180 insertions(+), 7 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b441d9c..b1d8256 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,16 @@ "Bash(cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | tail -20)", "Bash(npm run build 2>&1 | tail -15)", "Bash(cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | tail -15)", - "Bash(npm run build 2>&1 | tail -10)" + "Bash(npm run build 2>&1 | tail -10)", + "Bash(javap -cp /home/long/project/蚊子/target/classes /home/long/project/蚊子/target/classes/com/mosquito/project/permission/SysApprovalRecord.class | grep -i \"setId\\\\|setStatus\\\\|setFlowId\")", + "Bash(git commit -m \"$\\(cat <<'EOF'\nfeat\\(approval\\): 实现完整的审批流后端服务\n\n- 新增实体类: SysApprovalFlow, SysApprovalRecord, SysApprovalHistory\n- 新增Repositories: ApprovalFlowRepository, ApprovalRecordRepository, ApprovalHistoryRepository\n- 完整实现ApprovalFlowService: 提交审批、处理审批、取消审批等\n- 更新ApprovalController连接实际服务\n- 添加单元测试ApprovalFlowServiceTest\n- 更新Ralph状态文件 \\(Phase 3: 90%\\)\nEOF\n\\)\")", + "Bash(grep -l \"FAILURE\\\\|ERROR\" /home/long/project/蚊子/target/surefire-reports/*.txt | xargs -I{} basename {} .txt | head -15)", + "Bash(cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | tail -10)", + "Bash(mvn compile -q 2>&1 | tail -5 && cd /home/long/project/蚊子/frontend/admin && npm run build 2>&1 | tail -5)", + "Bash(git commit -m \"$\\(cat <<'EOF'\nfeat\\(business\\): 添加业务模块前后端服务\n\n后端Controllers:\n- AuditController: 审计日志API\n- SystemController: 系统配置API\n- RewardController: 奖励管理API\n- RiskController: 风险管理API\n\n前端Services:\n- activity.ts: 活动管理服务\n- user管理服务\n-Manage.ts: 用户 reward.ts: 奖励管理服务\n- risk.ts: 风险管理服务\n- audit.ts: 审计日志服务\n- systemConfig.ts: 系统配置服务\n- activity.ts: 活动类型定义\nEOF\n\\)\")", + "Bash(mvn compile -q 2>&1 | tail -5 && npm run build 2>&1 | tail -5)", + "mcp__serena__get_symbols_overview", + "Bash(npm run build 2>&1 | tail -20)" ], "deny": [] }, diff --git a/.ralph/state.md b/.ralph/state.md index 763915b..b41d7ef 100644 --- a/.ralph/state.md +++ b/.ralph/state.md @@ -5,8 +5,8 @@ - **Start Time**: 2026-03-04 - **Iterations**: 14 - **Total Tasks**: 136 -- **Completed Tasks**: 131 (96%) -- **Remaining Tasks**: 5 +- **Completed Tasks**: 133 (98%) +- **Remaining Tasks**: 3 ## Progress Summary diff --git a/docs/prd/开发任务追踪.md b/docs/prd/开发任务追踪.md index 6955744..76bff1e 100644 --- a/docs/prd/开发任务追踪.md +++ b/docs/prd/开发任务追踪.md @@ -178,7 +178,7 @@ | TASK-402 | 9.1.1 | KPI统计卡片 | 仪表盘 | dashboard.view | P0 | 1天 | ✅ | | TASK-403 | 9.1.1 | 数据图表 | 仪表盘 | dashboard.view | P0 | 1.5天 | ✅ | | TASK-404 | 9.1.1 | 待办事项 | 仪表盘 | dashboard.view | P0 | 0.5天 | ✅ | -| TASK-405 | 9.1.1 | 导出报表 | 仪表盘 | dashboard.export | P1 | 0.5天 | ⬜ | +| TASK-405 | 9.1.1 | 导出报表 | 仪表盘 | dashboard.export | P1 | 0.5天 | ✅ | ### 4.2 活动管理模块 @@ -278,8 +278,8 @@ | 任务ID | 任务名称 | 优先级 | 预计工时 | 状态 | |--------|----------|--------|----------|------| -| TASK-501 | 单元测试 - 权限服务 | P0 | 3天 | ⬜ | -| TASK-502 | 单元测试 - 审批流引擎 | P0 | 2天 | ⬜ | +| TASK-501 | 单元测试 - 权限服务 | P0 | 3天 | ✅ | +| TASK-502 | 单元测试 - 审批流引擎 | P0 | 2天 | ✅ | | TASK-503 | 集成测试 - 权限API | P0 | 2天 | ⬜ | | TASK-504 | 集成测试 - 审批流程 | P0 | 2天 | ⬜ | | TASK-505 | E2E测试 - 权限管理 | P0 | 2天 | ⬜ | diff --git a/frontend/admin/src/services/dashboard.ts b/frontend/admin/src/services/dashboard.ts index b24cf17..26b72fc 100644 --- a/frontend/admin/src/services/dashboard.ts +++ b/frontend/admin/src/services/dashboard.ts @@ -103,9 +103,58 @@ export async function getTodos(): Promise { return response.data.data } +/** + * 导出仪表盘数据 + */ +export async function exportDashboard(format: string = 'csv'): Promise { + const response = await dashboardApi.get('/dashboard/export', { + params: { format }, + responseType: 'blob' + }) + return response as unknown as Blob +} + +/** + * 导出KPI数据 + */ +export async function exportKpis(): Promise { + const response = await dashboardApi.get('/dashboard/kpis/export', { + responseType: 'blob' + }) + return response as unknown as Blob +} + +/** + * 导出活动数据 + */ +export async function exportActivities(): Promise { + const response = await dashboardApi.get('/dashboard/activities/export', { + responseType: 'blob' + }) + return response as unknown as Blob +} + +/** + * 下载文件工具函数 + */ +export function downloadBlob(blob: Blob, filename: string) { + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) +} + export default { getDashboard, getKpis, getActivitySummary, - getTodos + getTodos, + exportDashboard, + exportKpis, + exportActivities, + downloadBlob } diff --git a/src/main/java/com/mosquito/project/controller/DashboardController.java b/src/main/java/com/mosquito/project/controller/DashboardController.java index 96d698e..4976e15 100644 --- a/src/main/java/com/mosquito/project/controller/DashboardController.java +++ b/src/main/java/com/mosquito/project/controller/DashboardController.java @@ -6,10 +6,13 @@ import com.mosquito.project.dto.ApiResponse; import com.mosquito.project.service.ActivityService; import com.mosquito.project.permission.ApprovalFlowService; import com.mosquito.project.permission.SysApprovalRecord; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -232,4 +235,116 @@ public class DashboardController { return todos; } + + /** + * 导出仪表盘数据为CSV + */ + @GetMapping("/export") + public ResponseEntity exportDashboard(@RequestParam(defaultValue = "csv") String format) { + StringBuilder csv = new StringBuilder(); + csv.append("导出时间:").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\n\n"); + + // KPI数据 + csv.append("KPI指标\n"); + csv.append("指标,数值,状态,说明\n"); + for (Map kpi : buildKpiData()) { + csv.append(kpi.get("label")).append(",") + .append(kpi.get("value")).append(",") + .append(kpi.get("status")).append(",") + .append(kpi.get("hint")).append("\n"); + } + csv.append("\n"); + + // 活动数据 + csv.append("活动列表\n"); + csv.append("ID,名称,开始时间,结束时间,参与人数,分享数,转化数\n"); + for (Activity activity : activityService.getAllActivities()) { + csv.append(activity.getId()).append(",") + .append(escapeCsv(activity.getName())).append(",") + .append(activity.getStartTime() != null ? activity.getStartTime().toString() : "").append(",") + .append(activity.getEndTime() != null ? activity.getEndTime().toString() : "").append(","); + try { + ActivityStatsResponse stats = activityService.getActivityStats(activity.getId()); + csv.append(stats.getTotalParticipants()).append(",") + .append(stats.getTotalShares()).append(",") + .append(stats.getTotalParticipants()).append("\n"); + } catch (Exception e) { + csv.append("0,0,0\n"); + } + } + + byte[] body = csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8")); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"dashboard_export_" + + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".csv\""); + + return new ResponseEntity<>(body, headers, org.springframework.http.HttpStatus.OK); + } + + /** + * 导出KPI数据为CSV + */ + @GetMapping("/kpis/export") + public ResponseEntity exportKpis() { + StringBuilder csv = new StringBuilder(); + csv.append("指标,数值,状态,说明\n"); + for (Map kpi : buildKpiData()) { + csv.append(kpi.get("label")).append(",") + .append(kpi.get("value")).append(",") + .append(kpi.get("status")).append(",") + .append(kpi.get("hint")).append("\n"); + } + + byte[] body = csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8")); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"kpi_export_" + + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".csv\""); + + return new ResponseEntity<>(body, headers, org.springframework.http.HttpStatus.OK); + } + + /** + * 导出活动数据为CSV + */ + @GetMapping("/activities/export") + public ResponseEntity exportActivities() { + StringBuilder csv = new StringBuilder(); + csv.append("ID,名称,开始时间,结束时间,参与人数,分享数,转化数\n"); + + for (Activity activity : activityService.getAllActivities()) { + csv.append(activity.getId()).append(",") + .append(escapeCsv(activity.getName())).append(",") + .append(activity.getStartTime() != null ? activity.getStartTime().toString() : "").append(",") + .append(activity.getEndTime() != null ? activity.getEndTime().toString() : "").append(","); + try { + ActivityStatsResponse stats = activityService.getActivityStats(activity.getId()); + csv.append(stats.getTotalParticipants()).append(",") + .append(stats.getTotalShares()).append(",") + .append(stats.getTotalParticipants()).append("\n"); + } catch (Exception e) { + csv.append("0,0,0\n"); + } + } + + byte[] body = csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8")); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"activity_export_" + + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".csv\""); + + return new ResponseEntity<>(body, headers, org.springframework.http.HttpStatus.OK); + } + + /** + * 转义CSV特殊字符 + */ + private String escapeCsv(String value) { + if (value == null) return ""; + if (value.contains(",") || value.contains("\"") || value.contains("\n")) { + return "\"" + value.replace("\"", "\"\"") + "\""; + } + return value; + } }