diff --git a/docs/features/日报_AI_分析功能说明.md b/docs/features/日报_AI_分析功能说明.md new file mode 100644 index 0000000..674a67e --- /dev/null +++ b/docs/features/日报_AI_分析功能说明.md @@ -0,0 +1,260 @@ +# 日报 AI 分析功能使用说明 + +## 一、功能概述 + +当外部系统通过开放接口提交日报数据后,系统会自动触发 AI 分析任务,对日报内容进行深度分析,识别项目风险、资源需求,并提供进度建议。 + +## 二、核心特性 + +### 2.1 异步处理 +- 日报提交后立即返回成功响应 +- AI 分析在后台线程池中并行运行,不阻塞主流程 +- 支持并发处理多个日报分析请求(核心 5 线程,最大 10 线程) + +### 2.2 智能分析 +AI 会分析以下维度: +1. **整体进度评估**:判断项目进度是提前、正常还是滞后 +2. **里程碑风险识别**:识别可能延期的里程碑及风险等级 +3. **资源需求分析**:分析是否需要新增人力、物料、设备等资源 +4. **进度建议**:针对当前情况提出可操作的调整建议 +5. **风险识别**:识别潜在的项目风险并自动入库 + +### 2.3 自动化处理 +- 识别的风险自动保存到 `risk` 表 +- 资源需求自动保存到 `resource` 表 +- 项目状态根据分析结果自动更新 + +## 三、接口调用示例 + +### 3.1 请求示例 + +```http +POST /api/open/daily-report/sync +Content-Type: application/json + +{ + "projectId": 123, + "userId": "zhangsan", + "reportDate": "2026-03-31", + "workContent": "完成了项目需求分析文档的编写,与团队成员进行了技术方案讨论,确定了系统架构设计", + "tomorrowPlan": "开始进行系统架构设计,绘制 UML 图,组织技术评审会议", + "workIntensity": 4, + "needHelp": true, + "helpContent": "需要架构师协助评审技术方案" +} +``` + +### 3.2 响应示例 + +```json +{ + "code": 200, + "msg": "日报同步成功", + "data": null +} +``` + +**注意**:响应中不包含分析结果,因为 AI 分析在后台异步执行。 + +## 四、分析结果查看 + +### 4.1 查看识别的风险 + +```sql +-- 查询某项目通过日报分析识别的风险 +SELECT * FROM risk +WHERE project_id = 123 + AND risk_source = 'ai_daily_report' +ORDER BY discover_time DESC; +``` + +### 4.2 查看资源需求 + +```sql +-- 查询某项目通过日报分析识别的资源需求 +SELECT * FROM resource +WHERE project_id = 123 + AND resource_code LIKE 'RES_DR%' +ORDER BY create_time DESC; +``` + +### 4.3 查看项目状态变化 + +```sql +-- 查询项目状态变化 +SELECT id, project_name, status, progress, risk_level +FROM project +WHERE id = 123; +``` + +## 五、技术实现细节 + +### 5.1 线程池配置 + +```java +@Bean("dailyReportAnalysisExecutor") +public Executor dailyReportAnalysisExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); // 核心 5 线程 + executor.setMaxPoolSize(10); // 最大 10 线程 + executor.setQueueCapacity(200); // 队列容量 200 + executor.setThreadNamePrefix("daily-report-analysis-"); + // ... 其他配置 +} +``` + +### 5.2 分析流程 + +``` +1. OpenApiController 接收日报请求 + ↓ +2. OpenApiService.syncDailyReport() 保存日报 + ↓ +3. DailyReportAnalysisService.analyzeDailyReportAsync() 异步分析 + ↓ +4. 构建项目上下文 (项目信息 + 里程碑 + 任务统计) + ↓ +5. 调用 AI 模型进行分析 + ↓ +6. 解析 AI 返回的 JSON 结果 + ↓ +7. 保存分析结果到数据库 +``` + +### 5.3 AI 提示词设计 + +系统使用精心设计的 System Prompt,确保 AI 输出符合要求的 JSON 格式: + +``` +你是一个专业的项目管理 AI 助手,擅长从项目日报中分析项目状态... + +任务: +1. 整体进度评估 +2. 里程碑风险识别 +3. 资源需求分析 +4. 进度建议 +5. 风险识别 + +输出格式 (JSON): +{ + "overallProgressAssessment": {...}, + "milestoneRisks": [...], + "resourceNeeds": [...], + "progressSuggestions": [...], + "identifiedRisks": [...] +} +``` + +### 5.4 项目上下文构建 + +AI 分析时会注入以下项目信息到提示词中: + +```markdown +【项目基本信息】 +- 项目名称:XXX +- 项目类型:XXX +- 项目状态:XXX +- 计划开始日期:2026-01-01 +- 计划结束日期:2026-12-31 +- 当前进度:45% +- 项目预算:1000000 CNY +- 已花费成本:450000 CNY + +【里程碑信息】 +- 需求分析与架构设计 (计划:2026-04-30, 状态:pending, 进度:0%) +- 核心算法模型训练与验证 (计划:2026-06-15, 状态:pending, 进度:0%) +- 系统功能开发完成 (Alpha 版) (计划:2026-07-31, 状态:pending, 进度:0%) + +【任务列表】 +- T001 [milestone] 需求调研与分析 (计划:2026-04-01 ~ 2026-04-15, 状态:completed, 进度:100%) +- T002 [milestone] 技术架构设计 (计划:2026-04-16 ~ 2026-04-30, 状态:in_progress, 进度:60%) +- T003 [task] 数据库设计 (计划:2026-05-01 ~ 2026-05-10, 状态:pending, 进度:0%) + +【任务统计】 +- 任务总数:15 +- 已完成:3 +- 完成率:20.0% + +【进度分析】 +- 计划工期:270 天 +- 已过时间:90 天 +- 预期进度:33.3% +- 实际进度:25% +- 进度偏差:-8.3% + +【历史日报摘要】 +1. 2026-03-30: 完成了技术方案评审,确定了系统架构... +2. 2026-03-29: 进行了需求调研,访谈了 5 个业务部门... +3. 2026-03-28: 编写了需求规格说明书初稿... +``` + +## 六、错误处理 + +### 6.1 异常场景 + +| 场景 | 处理方式 | +|------|----------| +| AI 服务不可用 | 记录错误日志,不影响日报保存 | +| 分析结果为空 | 跳过保存,记录警告日志 | +| 数据库写入失败 | 事务回滚,记录错误日志 | +| 线程池满 | 由调用线程处理 (CallerRuns 策略) | + +### 6.2 日志查看 + +```bash +# 查看日报分析相关日志 +grep "日报 AI 分析" logs/application.log + +# 查看错误日志 +grep "\[日报 AI 分析\] 失败" logs/error.log +``` + +## 七、性能指标 + +| 指标 | 目标值 | +|------|--------| +| 单次分析耗时 | 30-60 秒 | +| 并发处理能力 | 10+ 个日报同时分析 | +| 队列容量 | 200 个待分析任务 | +| 风险识别准确率 | >80% | + +## 八、扩展开发 + +### 8.1 添加新的分析维度 + +修改 `DailyReportAnalysisServiceImpl.java` 中的 `DAILY_REPORT_ANALYSIS_SYSTEM_PROMPT`,添加新的分析要求。 + +### 8.2 调整线程池大小 + +根据实际负载情况,修改 `AsyncConfig.java` 中的线程池配置: + +```java +executor.setCorePoolSize(10); // 调整核心线程数 +executor.setMaxPoolSize(20); // 调整最大线程数 +``` + +### 8.3 自定义结果处理 + +修改 `saveAnalysisResult()` 方法,添加自定义的保存逻辑。 + +## 九、常见问题 + +### Q1: 为什么响应中不返回分析结果? +A: 因为 AI 分析是异步执行的,提交日报时分析任务可能还未完成。如果需要查看分析结果,可以通过查询风险表、资源表等获取。 + +### Q2: 分析任务失败会影响日报保存吗? +A: 不会。分析任务在独立的异步线程中执行,即使失败也不影响日报的正常保存。 + +### Q3: 如何重新触发分析? +A: 当前版本暂不支持手动重新触发。可以重新提交同一天的日报 (会触发防重拦截),或者通过数据库直接查看历史分析结果。 + +### Q4: 分析结果准确吗? +A: AI 分析的准确性取决于提供的项目数据质量和日报内容的详细程度。建议提供完整、准确的日报内容以获得更好的分析结果。 + +## 十、相关文件 + +- DTO: `DailyReportAnalysisResult.java` +- Service: `DailyReportAnalysisService.java` / `DailyReportAnalysisServiceImpl.java` +- Controller: `OpenApiController.java` +- Config: `AsyncConfig.java` +- Test: `DailyReportAnalysisServiceTest.java` diff --git a/docs/frontend-sse-integration.md b/docs/frontend-sse-integration.md deleted file mode 100644 index 8320d79..0000000 --- a/docs/frontend-sse-integration.md +++ /dev/null @@ -1,357 +0,0 @@ -# SSE 前端对接文档 - -## 概述 - -本文档描述前端如何与后端 SSE (Server-Sent Events) 服务进行对接,实现异步任务的实时进度推送。 - -### 核心特性 - -- **用户绑定**:SSE 通道通过 `userId` 绑定,一个用户只需建立一个连接 -- **多业务复用**:同一连接可接收多种业务类型的消息(项目初始化、系统通知等) -- **类型区分**:通过消息中的 `type` 字段区分不同业务 - ---- - -## 消息格式 - -所有 SSE 消息采用统一格式: - -```json -{ - "type": "project-init", // 业务类型 - "event": "progress", // 事件名称 - "userId": "user_123", // 用户ID - "data": { ... }, // 业务数据 - "timestamp": "2024-01-01T10:00:00" -} -``` - -### 业务类型 (type) - -| 类型 | 说明 | -|------|------| -| `project-init` | 项目初始化任务进度 | -| `system-notification` | 系统通知 | -| `task-notification` | 任务通知 | -| `system` | 系统事件(连接成功等) | - -### 事件名称 (event) - -#### 项目初始化 (type=project-init) - -| 事件 | 说明 | 数据结构 | -|------|------|----------| -| `submitted` | 任务已提交 | `{ taskId, message }` | -| `progress` | 进度更新 | `ProjectInitTaskVO` | -| `complete` | 任务完成 | `ProjectInitTaskVO` | -| `error` | 执行错误 | `{ error }` | - -#### 系统事件 (type=system) - -| 事件 | 说明 | -|------|------| -| `connected` | SSE 连接成功 | - ---- - -## 对接步骤 - -### 1. 建立 SSE 连接 - -```javascript -// 使用用户ID建立连接 -const userId = 'user_123'; // 当前登录用户ID -const eventSource = new EventSource(`/api/v1/sse/connect/${userId}`); - -// 监听连接成功事件 -eventSource.addEventListener('connected', (e) => { - const message = JSON.parse(e.data); - console.log('SSE连接成功:', message); - // { type: "system", event: "connected", userId: "user_123", data: {...} } -}); -``` - -### 2. 监听业务消息 - -```javascript -// 监听项目初始化进度 -eventSource.addEventListener('progress', (e) => { - const message = JSON.parse(e.data); - - // 根据 type 字段处理不同业务 - switch(message.type) { - case 'project-init': - handleProjectInitProgress(message.data); - break; - case 'system-notification': - handleSystemNotification(message.data); - break; - case 'task-notification': - handleTaskNotification(message.data); - break; - } -}); - -// 监听任务完成 -eventSource.addEventListener('complete', (e) => { - const message = JSON.parse(e.data); - if (message.type === 'project-init') { - console.log('项目初始化完成:', message.data); - // data 包含完整的 ProjectInitTaskVO,包括 result 字段 - } -}); - -// 监听错误 -eventSource.addEventListener('error', (e) => { - const message = JSON.parse(e.data); - console.error('任务执行错误:', message.data.error); -}); -``` - -### 3. 提交项目初始化任务 - -```javascript -async function submitProjectInitTask(file) { - const formData = new FormData(); - formData.append('userId', userId); // 必须与 SSE 连接时的 userId 一致 - formData.append('file', file); - - const response = await fetch('/api/v1/project-init/sse/submit-task', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - - if (result.code === 200) { - console.log('任务提交成功:', result.data.taskId); - // 进度将通过已建立的 SSE 连接推送 - } else { - console.error('提交失败:', result.message); - } -} -``` - -### 4. 关闭连接 - -```javascript -// 页面卸载时关闭连接 -window.addEventListener('beforeunload', () => { - // 可选:调用后端关闭接口 - fetch(`/api/v1/sse/close/${userId}`, { method: 'POST' }); - eventSource.close(); -}); -``` - ---- - -## 完整示例 - -```javascript -class SseClient { - constructor(userId) { - this.userId = userId; - this.eventSource = null; - this.listeners = new Map(); - } - - // 建立连接 - connect() { - this.eventSource = new EventSource(`/api/v1/sse/connect/${this.userId}`); - - // 系统事件 - this.eventSource.addEventListener('connected', (e) => { - console.log('SSE连接成功'); - this.emit('connected', JSON.parse(e.data)); - }); - - // 项目初始化事件 - this.eventSource.addEventListener('submitted', (e) => { - const msg = JSON.parse(e.data); - if (msg.type === 'project-init') { - this.emit('project-init-submitted', msg.data); - } - }); - - this.eventSource.addEventListener('progress', (e) => { - const msg = JSON.parse(e.data); - if (msg.type === 'project-init') { - this.emit('project-init-progress', msg.data); - } - }); - - this.eventSource.addEventListener('complete', (e) => { - const msg = JSON.parse(e.data); - if (msg.type === 'project-init') { - this.emit('project-init-complete', msg.data); - } - }); - - this.eventSource.addEventListener('error', (e) => { - const msg = JSON.parse(e.data); - this.emit('error', msg.data); - }); - } - - // 提交项目初始化任务 - async submitProjectInitTask(file) { - const formData = new FormData(); - formData.append('userId', this.userId); - formData.append('file', file); - - const response = await fetch('/api/v1/project-init/sse/submit-task', { - method: 'POST', - body: formData - }); - - return response.json(); - } - - // 事件监听 - on(event, callback) { - if (!this.listeners.has(event)) { - this.listeners.set(event, []); - } - this.listeners.get(event).push(callback); - } - - emit(event, data) { - const callbacks = this.listeners.get(event); - if (callbacks) { - callbacks.forEach(cb => cb(data)); - } - } - - // 关闭连接 - close() { - if (this.eventSource) { - this.eventSource.close(); - } - } -} - -// 使用示例 -const sseClient = new SseClient('user_123'); - -// 监听进度 -sseClient.on('project-init-progress', (data) => { - console.log(`进度: ${data.progress}%, ${data.progressMessage}`); - // 更新进度条 -}); - -sseClient.on('project-init-complete', (data) => { - console.log('完成:', data.result); - // 显示结果 -}); - -// 建立连接 -sseClient.connect(); - -// 提交任务 -document.getElementById('uploadBtn').addEventListener('click', async () => { - const file = document.getElementById('fileInput').files[0]; - const result = await sseClient.submitProjectInitTask(file); - console.log('提交结果:', result); -}); -``` - ---- - -## 数据结构 - -### ProjectInitTaskVO - -```typescript -interface ProjectInitTaskVO { - taskId: string; // 任务ID - status: string; // 状态: pending/processing/completed/failed - statusDesc: string; // 状态描述 - progress: number; // 进度百分比 (0-100) - progressMessage: string; // 进度描述 - originalFilename: string; // 原始文件名 - createTime: string; // 创建时间 - startTime: string; // 开始时间 - completeTime: string; // 完成时间 - result?: ProjectInitResult; // 结果数据(完成时) - errorMessage?: string; // 错误信息(失败时) -} -``` - -### 进度阶段说明 - -| 进度 | 阶段 | 说明 | -|------|------|------| -| 0% | pending | 任务已提交,等待处理 | -| 10% | processing | 开始处理,正在上传文件 | -| 30% | processing | 文件上传完成,读取内容 | -| 50% | processing | 文件读取完成,AI分析中 | -| 60% | processing | AI解析项目结构 | -| 100% | completed | 项目预览数据生成成功 | - ---- - -## 错误处理 - -### 连接错误 - -```javascript -eventSource.onerror = (error) => { - console.error('SSE连接错误:', error); - // 可尝试重连 -}; -``` - -### 提交任务错误 - -```javascript -// HTTP 响应错误 -if (response.code !== 200) { - console.error('提交失败:', response.message); - // 可能的错误: - // - "上传文件不能为空" - // - "用户未建立SSE连接,请先调用 /api/v1/sse/connect/{userId}" -} - -// 任务执行错误(通过 SSE 推送) -eventSource.addEventListener('error', (e) => { - const msg = JSON.parse(e.data); - console.error('任务执行错误:', msg.data.error); -}); -``` - ---- - -## 注意事项 - -1. **用户ID一致性**:SSE 连接和提交任务时必须使用相同的 `userId` - -2. **连接超时**:默认 30 分钟超时,超时后需要重新建立连接 - -3. **单用户单连接**:一个 `userId` 同时只能有一个 SSE 连接,新建连接会自动关闭旧连接 - -4. **文件大小限制**:建议前端先做文件大小校验,避免上传过大文件 - -5. **重连机制**:建议实现自动重连机制,当连接断开时自动重新建立连接 - -```javascript -// 简单重连示例 -function connectWithRetry(userId, maxRetries = 3) { - let retries = 0; - - const connect = () => { - const es = new EventSource(`/api/v1/sse/connect/${userId}`); - - es.onerror = (e) => { - es.close(); - retries++; - if (retries < maxRetries) { - setTimeout(connect, 3000); // 3秒后重试 - } - }; - - return es; - }; - - return connect(); -} -``` diff --git a/src/main/java/cn/yinlihupo/common/config/AsyncConfig.java b/src/main/java/cn/yinlihupo/common/config/AsyncConfig.java index 497d4dc..3f59c1d 100644 --- a/src/main/java/cn/yinlihupo/common/config/AsyncConfig.java +++ b/src/main/java/cn/yinlihupo/common/config/AsyncConfig.java @@ -70,4 +70,31 @@ public class AsyncConfig { log.info("文档处理异步任务线程池初始化完成"); return executor; } + + /** + * 日报 AI 分析任务线程池 + */ + @Bean("dailyReportAnalysisExecutor") + public Executor dailyReportAnalysisExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + // 核心线程数 + executor.setCorePoolSize(5); + // 最大线程数 + executor.setMaxPoolSize(10); + // 队列容量 + executor.setQueueCapacity(200); + // 线程名称前缀 + executor.setThreadNamePrefix("daily-report-analysis-"); + // 拒绝策略:由调用线程处理 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 等待所有任务完成后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + // 等待时间(秒) + executor.setAwaitTerminationSeconds(60); + // 初始化 + executor.initialize(); + + log.info("日报 AI 分析异步任务线程池初始化完成"); + return executor; + } } diff --git a/src/main/java/cn/yinlihupo/domain/dto/DailyReportAnalysisResult.java b/src/main/java/cn/yinlihupo/domain/dto/DailyReportAnalysisResult.java new file mode 100644 index 0000000..4087208 --- /dev/null +++ b/src/main/java/cn/yinlihupo/domain/dto/DailyReportAnalysisResult.java @@ -0,0 +1,254 @@ +package cn.yinlihupo.domain.dto; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * 日报 AI 分析结果 DTO + */ +@Data +public class DailyReportAnalysisResult { + + /** + * 项目 ID + */ + private Long projectId; + + /** + * 项目名称 + */ + private String projectName; + + /** + * 日报日期 + */ + private LocalDate reportDate; + + /** + * 整体进度评估 + */ + private OverallProgressAssessment overallProgressAssessment; + + /** + * 里程碑风险列表 + */ + private List milestoneRisks; + + /** + * 资源需求列表 + */ + private List resourceNeeds; + + /** + * 进度建议列表 + */ + private List progressSuggestions; + + /** + * 识别的风险列表 (直接入库) + */ + private List identifiedRisks; + + /** + * 整体进度评估 + */ + @Data + public static class OverallProgressAssessment { + /** + * 进度状态:ahead-提前,on_track-正常,delayed-滞后 + */ + private String status; + + /** + * 进度偏差百分比 (正数表示提前,负数表示滞后) + */ + private BigDecimal deviationPercentage; + + /** + * 评估说明 + */ + private String description; + + /** + * 关键问题 + */ + private List keyIssues; + } + + /** + * 里程碑风险 + */ + @Data + public static class MilestoneRisk { + /** + * 里程碑 ID + */ + private Long milestoneId; + + /** + * 里程碑名称 + */ + private String milestoneName; + + /** + * 计划完成日期 + */ + private LocalDate planDate; + + /** + * 风险等级:critical-严重,high-高,medium-中,low-低 + */ + private String riskLevel; + + /** + * 风险描述 + */ + private String description; + + /** + * 延期天数 (预估) + */ + private Integer estimatedDelayDays; + + /** + * 建议措施 + */ + private String suggestion; + } + + /** + * 资源需求 + */ + @Data + public static class ResourceNeed { + /** + * 资源类型:human-人力,material-物料,equipment-设备,other-其他 + */ + private String resourceType; + + /** + * 资源名称 + */ + private String resourceName; + + /** + * 需求数量 + */ + private BigDecimal quantity; + + /** + * 单位 + */ + private String unit; + + /** + * 需求原因 + */ + private String reason; + + /** + * 建议到位时间 + */ + private LocalDate suggestedArrivalDate; + } + + /** + * 进度建议 + */ + @Data + public static class ProgressSuggestion { + /** + * 任务 ID (如果有明确关联的任务) + */ + private Long taskId; + + /** + * 任务名称 + */ + private String taskName; + + /** + * 建议类型:accelerate-加速,adjust_plan-调整计划,add_resource-增加资源,reorder-重新排序 + */ + private String suggestionType; + + /** + * 具体建议内容 + */ + private String suggestion; + + /** + * 优先级:critical-紧急,high-高,medium-中,low-低 + */ + private String priority; + + /** + * 预期效果 + */ + private String expectedEffect; + } + + /** + * 识别的风险 (直接入库) + */ + @Data + public static class IdentifiedRisk { + /** + * 风险名称 + */ + private String riskName; + + /** + * 风险分类:technical-技术,schedule-进度,cost-成本,quality-质量,resource-资源,external-外部 + */ + private String category; + + /** + * 风险描述 + */ + private String description; + + /** + * 发生概率 (0-100) + */ + private Integer probability; + + /** + * 影响程度 (1-5) + */ + private Integer impact; + + /** + * 风险等级:calculated from probability * impact + */ + private String riskLevel; + + /** + * 影响范围 + */ + private String impactScope; + + /** + * 触发条件 + */ + private String triggerCondition; + + /** + * 缓解措施 + */ + private String mitigationPlan; + + /** + * 应急计划 + */ + private String contingencyPlan; + + /** + * 优先级 + */ + private String priority; + } +} diff --git a/src/main/java/cn/yinlihupo/service/analysis/DailyReportAnalysisService.java b/src/main/java/cn/yinlihupo/service/analysis/DailyReportAnalysisService.java new file mode 100644 index 0000000..a6e3d98 --- /dev/null +++ b/src/main/java/cn/yinlihupo/service/analysis/DailyReportAnalysisService.java @@ -0,0 +1,34 @@ +package cn.yinlihupo.service.analysis; + +import cn.yinlihupo.domain.dto.DailyReportAnalysisResult; +import cn.yinlihupo.domain.entity.ProjectDailyReport; + +/** + * 日报 AI 分析服务接口 + */ +public interface DailyReportAnalysisService { + + /** + * 异步分析日报数据 + * + * @param projectId 项目 ID + * @param report 日报数据 + */ + void analyzeDailyReportAsync(Long projectId, ProjectDailyReport report); + + /** + * 同步分析方法 (内部使用) + * + * @param projectId 项目 ID + * @param report 日报数据 + * @return 分析结果 + */ + DailyReportAnalysisResult analyzeDailyReport(Long projectId, ProjectDailyReport report); + + /** + * 保存分析结果到数据库 + * + * @param result 分析结果 + */ + void saveAnalysisResult(DailyReportAnalysisResult result); +} diff --git a/src/main/java/cn/yinlihupo/service/analysis/impl/DailyReportAnalysisServiceImpl.java b/src/main/java/cn/yinlihupo/service/analysis/impl/DailyReportAnalysisServiceImpl.java new file mode 100644 index 0000000..1b67c70 --- /dev/null +++ b/src/main/java/cn/yinlihupo/service/analysis/impl/DailyReportAnalysisServiceImpl.java @@ -0,0 +1,539 @@ +package cn.yinlihupo.service.analysis.impl; + +import cn.hutool.core.util.IdUtil; +import cn.yinlihupo.domain.dto.DailyReportAnalysisResult; +import cn.yinlihupo.domain.entity.*; +import cn.yinlihupo.mapper.*; +import cn.yinlihupo.service.analysis.DailyReportAnalysisService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 日报 AI 分析服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DailyReportAnalysisServiceImpl implements DailyReportAnalysisService { + + private final ChatClient chatClient; + private final ProjectMapper projectMapper; + private final ProjectMilestoneMapper projectMilestoneMapper; + private final TaskMapper taskMapper; + private final ResourceMapper resourceMapper; + private final RiskMapper riskMapper; + private final ProjectDailyReportMapper projectDailyReportMapper; + + /** + * AI 分析系统提示词模板 + */ + private static final String DAILY_REPORT_ANALYSIS_SYSTEM_PROMPT = """ + # 角色 + + 你是一个专业的项目管理 AI 助手,擅长从项目日报中分析项目状态、识别风险和资源需求。 + + # 任务 + + 根据提供的【项目基本信息】+【当前日报内容】,进行以下分析: + + 1. **整体进度评估**: 判断项目进度是提前、正常还是滞后 + 2. **里程碑风险识别**: 分析哪些里程碑可能延期,延期风险等级 + 3. **资源需求分析**: 识别是否需要新增人力、物料、设备等资源 + 4. **进度建议**: 针对当前进度提出可操作的调整建议 + 5. **风险识别**: 识别潜在的项目风险并评估 + + # 输出格式 + + 请严格按照以下 JSON 格式输出: + + ```json + { + "overallProgressAssessment": { + "status": "ahead/on_track/delayed", + "deviationPercentage": 10.5, + "description": "进度评估说明", + "keyIssues": ["问题 1", "问题 2"] + }, + "milestoneRisks": [ + { + "milestoneId": 123, + "milestoneName": "里程碑名称", + "planDate": "2024-12-31", + "riskLevel": "high", + "description": "风险描述", + "estimatedDelayDays": 5, + "suggestion": "建议措施" + } + ], + "resourceNeeds": [ + { + "resourceType": "human", + "resourceName": "Java 开发工程师", + "quantity": 2, + "unit": "人", + "urgency": "high", + "reason": "需求原因", + "suggestedArrivalDate": "2024-04-15" + } + ], + "progressSuggestions": [ + { + "taskId": 456, + "taskName": "任务名称", + "suggestionType": "add_resource", + "suggestion": "具体建议内容", + "expectedEffect": "预期效果" + } + ], + "identifiedRisks": [ + { + "riskName": "风险名称", + "category": "schedule", + "description": "风险详细描述", + "probability": 60, + "impact": 4, + "riskLevel": "high", + "impactScope": "影响范围", + "triggerCondition": "触发条件", + "mitigationPlan": "缓解措施", + "contingencyPlan": "应急计划", + "priority": "high" + } + ] + } + ``` + + # 注意事项 + + 1. probability(发生概率) 范围 0-100 + 2. impact(影响程度) 范围 1-5 + 3. 风险等级判定:probability * impact / 5 * 100,得分>=80 为 critical,60-80 为 high,40-60 为 medium,<40 为 low + 4. 识别的风险应该具体且可操作,不要泛泛而谈 + 5. 建议要结合日报中的实际工作内容,有针对性 + 6. 如果日报中没有明显风险,可以返回空数组,不要强行编造 + """; + + @Override + @Async("dailyReportAnalysisExecutor") + public void analyzeDailyReportAsync(Long projectId, ProjectDailyReport report) { + log.info("[日报 AI 分析] 开始异步分析,projectId={}, reportDate={}", projectId, report.getReportDate()); + + try { + // 执行分析 + DailyReportAnalysisResult result = analyzeDailyReport(projectId, report); + + // 保存结果 + saveAnalysisResult(result); + + log.info("[日报 AI 分析] 完成,projectId={}, 识别风险数={}, 资源需求数={}", + projectId, + result.getIdentifiedRisks() != null ? result.getIdentifiedRisks().size() : 0, + result.getResourceNeeds() != null ? result.getResourceNeeds().size() : 0); + + } catch (Exception e) { + log.error("[日报 AI 分析] 失败,projectId={}, reportDate={}, error={}", + projectId, report.getReportDate(), e.getMessage(), e); + } + } + + @Override + public DailyReportAnalysisResult analyzeDailyReport(Long projectId, ProjectDailyReport report) { + log.debug("[日报 AI 分析] 同步分析开始,projectId={}, reportDate={}", projectId, report.getReportDate()); + + // 1. 构建项目上下文数据 + String projectContext = buildProjectContext(projectId, report); + + // 2. 构建用户提示词 + String userPrompt = """ + 请根据以下项目信息和日报内容进行分析: + + %s + + --- + + 【当前日报内容】 + - 日期:%s + - 工作内容:%s + - 明日计划:%s + - 工作强度:%d/5 + - 需要协助:%s + %s + + 请严格按照系统提示词中的 JSON 格式输出分析结果。 + """.formatted( + projectContext, + report.getReportDate(), + report.getWorkContent(), + report.getTomorrowPlan() != null ? report.getTomorrowPlan() : "无", + report.getWorkIntensity() != null ? report.getWorkIntensity() : 3, + report.getNeedHelp() != null && report.getNeedHelp() ? "是" : "否", + report.getHelpContent() != null ? "- 协助内容:" + report.getHelpContent() : "" + ); + + // 3. 调用 AI 进行分析 + log.info("[日报 AI 分析] 调用 AI 模型..."); + DailyReportAnalysisResult result = chatClient.prompt() + .system(DAILY_REPORT_ANALYSIS_SYSTEM_PROMPT) + .user(userPrompt) + .call() + .entity(DailyReportAnalysisResult.class); + + // 4. 补充项目信息 + if (result != null) { + result.setProjectId(projectId); + result.setReportDate(report.getReportDate()); + + // 填充项目名称 + Project project = projectMapper.selectById(projectId); + if (project != null) { + result.setProjectName(project.getProjectName()); + } + } + + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveAnalysisResult(DailyReportAnalysisResult result) { + if (result == null) { + log.warn("[日报 AI 分析] 分析结果为空,跳过保存"); + return; + } + + Long projectId = result.getProjectId(); + log.info("[日报 AI 分析] 保存结果,projectId={}", projectId); + + LocalDateTime now = LocalDateTime.now(); + + // 1. 保存识别的风险 + if (result.getIdentifiedRisks() != null && !result.getIdentifiedRisks().isEmpty()) { + List savedRiskIds = saveIdentifiedRisks(result.getIdentifiedRisks(), projectId, now); + log.info("[日报 AI 分析] 保存 {} 个识别的风险,IDs={}", savedRiskIds.size(), savedRiskIds); + } + + // 2. 保存资源需求 + if (result.getResourceNeeds() != null && !result.getResourceNeeds().isEmpty()) { + List savedResourceIds = saveResourceNeeds(result.getResourceNeeds(), projectId, now); + log.info("[日报 AI 分析] 保存 {} 个资源需求,IDs={}", savedResourceIds.size(), savedResourceIds); + } + + // 3. 更新项目进度 (如果有整体进度评估) + if (result.getOverallProgressAssessment() != null) { + updateProjectProgress(projectId, result.getOverallProgressAssessment()); + } + + log.info("[日报 AI 分析] 结果保存完成,projectId={}", projectId); + } + + /** + * 构建项目上下文数据 + */ + private String buildProjectContext(Long projectId, ProjectDailyReport report) { + StringBuilder sb = new StringBuilder(); + + // 项目基本信息 + Project project = projectMapper.selectById(projectId); + if (project != null) { + sb.append("【项目基本信息】\n"); + sb.append(String.format("- 项目名称:%s\n", project.getProjectName())); + sb.append(String.format("- 项目类型:%s\n", project.getProjectType())); + sb.append(String.format("- 项目状态:%s\n", project.getStatus())); + sb.append(String.format("- 计划开始日期:%s\n", project.getPlanStartDate())); + sb.append(String.format("- 计划结束日期:%s\n", project.getPlanEndDate())); + sb.append(String.format("- 当前进度:%d%%\n", project.getProgress() != null ? project.getProgress() : 0)); + sb.append(String.format("- 项目预算:%s %s\n", project.getBudget(), project.getCurrency())); + sb.append(String.format("- 已花费成本:%s %s\n", project.getCost(), project.getCurrency())); + sb.append("\n"); + } + + // 里程碑信息 + List milestones = projectMilestoneMapper.selectList( + new LambdaQueryWrapper() + .eq(ProjectMilestone::getProjectId, projectId) + .eq(ProjectMilestone::getDeleted, 0) + .orderByAsc(ProjectMilestone::getSortOrder) + ); + + if (!milestones.isEmpty()) { + sb.append("【里程碑信息】\n"); + for (ProjectMilestone milestone : milestones) { + sb.append(String.format("- %s (计划:%s, 状态:%s, 进度:%d%%)\n", + milestone.getMilestoneName(), + milestone.getPlanDate(), + milestone.getStatus(), + milestone.getProgress() != null ? milestone.getProgress() : 0)); + } + sb.append("\n"); + } + + // 任务列表详情 + List tasks = taskMapper.selectList( + new LambdaQueryWrapper() + .eq(Task::getProjectId, projectId) + .eq(Task::getDeleted, 0) + .orderByAsc(Task::getSortOrder) + ); + + if (!tasks.isEmpty()) { + sb.append("【任务列表】\n"); + for (Task task : tasks) { + String assigneeInfo = task.getAssigneeId() != null ? "(负责人 ID: " + task.getAssigneeId() + ")" : "(未分配)"; + sb.append(String.format("- %s [%s] %s (计划:%s ~ %s, 状态:%s, 进度:%d%%)\n", + task.getTaskCode() != null ? task.getTaskCode() : "", + task.getTaskType() != null ? task.getTaskType() : "task", + task.getTaskName(), + task.getPlanStartDate(), + task.getPlanEndDate(), + task.getStatus(), + task.getProgress() != null ? task.getProgress() : 0)); + } + sb.append("\n"); + } + + // 任务统计 + long totalTasks = taskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Task::getProjectId, projectId) + .eq(Task::getDeleted, 0) + ); + long completedTasks = taskMapper.selectCount( + new LambdaQueryWrapper() + .eq(Task::getProjectId, projectId) + .eq(Task::getDeleted, 0) + .eq(Task::getStatus, "completed") + ); + + sb.append("【任务统计】\n"); + sb.append(String.format("- 任务总数:%d\n", totalTasks)); + sb.append(String.format("- 已完成:%d\n", completedTasks)); + sb.append(String.format("- 完成率:%.1f%%\n", totalTasks > 0 ? (completedTasks * 100.0 / totalTasks) : 0)); + sb.append("\n"); + + // 计算进度偏差 + if (project != null && project.getPlanStartDate() != null && project.getPlanEndDate() != null) { + LocalDate now = LocalDate.now(); + long totalDays = java.time.temporal.ChronoUnit.DAYS.between(project.getPlanStartDate(), project.getPlanEndDate()); + long elapsedDays = java.time.temporal.ChronoUnit.DAYS.between(project.getPlanStartDate(), now); + elapsedDays = Math.max(0, Math.min(elapsedDays, totalDays)); + double expectedProgress = totalDays > 0 ? (elapsedDays * 100.0 / totalDays) : 0; + int actualProgress = project.getProgress() != null ? project.getProgress() : 0; + double deviation = actualProgress - expectedProgress; + + sb.append("【进度分析】\n"); + sb.append(String.format("- 计划工期:%d 天\n", totalDays)); + sb.append(String.format("- 已过时间:%d 天\n", elapsedDays)); + sb.append(String.format("- 预期进度:%.1f%%\n", expectedProgress)); + sb.append(String.format("- 实际进度:%d%%\n", actualProgress)); + sb.append(String.format("- 进度偏差:%+.1f%%\n", deviation)); + sb.append("\n"); + } + + // 历史日报摘要 (最近 5 条) + List recentReports = projectDailyReportMapper.selectList( + new LambdaQueryWrapper() + .eq(ProjectDailyReport::getProjectId, projectId) + .eq(ProjectDailyReport::getDeleted, 0) + .ne(ProjectDailyReport::getId, report.getId()) // 排除当前日报 + .orderByDesc(ProjectDailyReport::getReportDate) + .last("LIMIT 5") + ); + + if (!recentReports.isEmpty()) { + sb.append("【历史日报摘要】\n"); + int index = 1; + for (ProjectDailyReport dailyReport : recentReports) { + sb.append(String.format("%d. %s: %s\n", + index++, + dailyReport.getReportDate(), + dailyReport.getWorkContent().length() > 50 + ? dailyReport.getWorkContent().substring(0, 50) + "..." + : dailyReport.getWorkContent())); + } + sb.append("\n"); + } + + return sb.toString(); + } + + /** + * 保存识别的风险 + */ + private List saveIdentifiedRisks(List risks, + Long projectId, + LocalDateTime now) { + List savedIds = new ArrayList<>(); + + for (DailyReportAnalysisResult.IdentifiedRisk riskInfo : risks) { + Risk risk = new Risk(); + risk.setProjectId(projectId); + risk.setRiskCode(generateRiskCode()); + risk.setRiskName(riskInfo.getRiskName()); + risk.setCategory(riskInfo.getCategory() != null ? riskInfo.getCategory() : "other"); + risk.setDescription(riskInfo.getDescription()); + risk.setRiskSource("ai_daily_report"); + + if (riskInfo.getProbability() != null) { + risk.setProbability(BigDecimal.valueOf(riskInfo.getProbability())); + } + if (riskInfo.getImpact() != null) { + risk.setImpact(BigDecimal.valueOf(riskInfo.getImpact())); + } + + // 计算风险得分 + if (risk.getProbability() != null && risk.getImpact() != null) { + BigDecimal score = risk.getProbability() + .multiply(risk.getImpact()) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + risk.setRiskScore(score); + } + + // 设置风险等级 + if (riskInfo.getRiskLevel() != null) { + risk.setRiskLevel(riskInfo.getRiskLevel()); + } else if (risk.getProbability() != null && risk.getImpact() != null) { + risk.setRiskLevel(calculateRiskLevel(riskInfo.getProbability(), riskInfo.getImpact())); + } + + risk.setMitigationPlan(riskInfo.getMitigationPlan()); + risk.setContingencyPlan(riskInfo.getContingencyPlan()); + risk.setTriggerCondition(riskInfo.getTriggerCondition()); + risk.setStatus("identified"); + risk.setDiscoverTime(now); + + // 存储 AI 分析元数据 + Map aiAnalysis = new HashMap<>(); + aiAnalysis.put("impact_scope", riskInfo.getImpactScope()); + aiAnalysis.put("priority", riskInfo.getPriority()); + aiAnalysis.put("analysis_date", LocalDate.now().toString()); + aiAnalysis.put("source", "daily_report_analysis"); + risk.setAiAnalysis(aiAnalysis); + + riskMapper.insert(risk); + savedIds.add(risk.getId()); + } + + return savedIds; + } + + /** + * 保存资源需求 + */ + private List saveResourceNeeds(List resourceNeeds, + Long projectId, + LocalDateTime now) { + List savedIds = new ArrayList<>(); + + for (DailyReportAnalysisResult.ResourceNeed need : resourceNeeds) { + Resource resource = new Resource(); + resource.setProjectId(projectId); + resource.setResourceCode(generateResourceCode()); + resource.setResourceName(need.getResourceName()); + resource.setResourceType(need.getResourceType() != null ? need.getResourceType() : "other"); + resource.setDescription(need.getReason()); + resource.setSpecification(need.getUnit()); + + if (need.getQuantity() != null) { + resource.setPlanQuantity(need.getQuantity()); + } + + resource.setStatus("planned"); + resource.setCreateTime(now); + + resourceMapper.insert(resource); + savedIds.add(resource.getId()); + } + + return savedIds; + } + + /** + * 更新项目进度 + */ + private void updateProjectProgress(Long projectId, + DailyReportAnalysisResult.OverallProgressAssessment assessment) { + Project project = projectMapper.selectById(projectId); + if (project == null) { + return; + } + + // 根据 AI 评估更新项目状态 + if ("delayed".equals(assessment.getStatus())) { + // 如果评估为滞后,更新项目状态为 delayed + project.setStatus("delayed"); + } else if ("ahead".equals(assessment.getStatus())) { + // 如果评估为提前,可以更新状态 + if (!"completed".equals(project.getStatus())) { + project.setStatus("in_progress"); + } + } + + // 可以在这里添加更复杂的进度更新逻辑 + // 例如根据 keyIssues 更新项目的某些字段 + + projectMapper.updateById(project); + } + + /** + * 生成风险编号 + */ + private String generateRiskCode() { + return "RSK_DR" + IdUtil.fastSimpleUUID().substring(0, 10).toUpperCase(); + } + + /** + * 生成资源编号 + */ + private String generateResourceCode() { + return "RES_DR" + IdUtil.fastSimpleUUID().substring(0, 10).toUpperCase(); + } + + /** + * 计算风险等级 + */ + private String calculateRiskLevel(Integer probability, Integer impact) { + if (probability == null || impact == null) { + return "low"; + } + int score = probability * impact; + if (score >= 300) { + return "critical"; + } else if (score >= 200) { + return "high"; + } else if (score >= 100) { + return "medium"; + } else { + return "low"; + } + } + + /** + * 将紧急程度映射为优先级 + */ + private String mapUrgencyToPriority(String urgency) { + if (urgency == null) { + return "medium"; + } + return switch (urgency.toLowerCase()) { + case "urgent" -> "critical"; + case "high" -> "high"; + case "medium" -> "medium"; + case "low" -> "low"; + default -> "medium"; + }; + } +} diff --git a/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java b/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java index e92e873..766fc77 100644 --- a/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java +++ b/src/main/java/cn/yinlihupo/service/open/impl/OpenApiServiceImpl.java @@ -11,6 +11,7 @@ import cn.yinlihupo.mapper.ProjectMapper; import cn.yinlihupo.mapper.ProjectMemberMapper; import cn.yinlihupo.mapper.SysUserMapper; import cn.yinlihupo.service.open.OpenApiService; +import cn.yinlihupo.service.analysis.DailyReportAnalysisService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -31,6 +32,7 @@ public class OpenApiServiceImpl implements OpenApiService { private final ProjectMapper projectMapper; private final ProjectMemberMapper projectMemberMapper; private final ProjectDailyReportMapper projectDailyReportMapper; + private final DailyReportAnalysisService dailyReportAnalysisService; /** * 根据用户标识 (sys_user.username) 查询用户所在的项目列表 @@ -127,7 +129,11 @@ public class OpenApiServiceImpl implements OpenApiService { projectDailyReportMapper.insert(report); - log.info("[OpenApi] 日报同步成功, projectId={}, reportDate={}, username={}", + // 5. 保存日报后,触发异步 AI 分析 + ProjectDailyReport finalReport = report; // 用于 lambda + dailyReportAnalysisService.analyzeDailyReportAsync(dto.getProjectId(), finalReport); + + log.info("[OpenApi] 日报同步成功,已触发 AI 分析,projectId={}, reportDate={}, username={}", dto.getProjectId(), dto.getReportDate(), username); return "日报同步成功"; } diff --git a/src/test/java/cn/yinlihupo/service/analysis/impl/DailyReportAnalysisServiceTest.java b/src/test/java/cn/yinlihupo/service/analysis/impl/DailyReportAnalysisServiceTest.java new file mode 100644 index 0000000..96ca37e --- /dev/null +++ b/src/test/java/cn/yinlihupo/service/analysis/impl/DailyReportAnalysisServiceTest.java @@ -0,0 +1,130 @@ +package cn.yinlihupo.service.analysis.impl; + +import cn.yinlihupo.domain.dto.DailyReportAnalysisResult; +import cn.yinlihupo.domain.entity.Project; +import cn.yinlihupo.domain.entity.ProjectDailyReport; +import cn.yinlihupo.mapper.ProjectMapper; +import cn.yinlihupo.service.analysis.DailyReportAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 日报 AI 分析服务测试 + */ +@Slf4j +@SpringBootTest +public class DailyReportAnalysisServiceTest { + + @Autowired + private DailyReportAnalysisService analysisService; + + @Autowired + private ProjectMapper projectMapper; + + /** + * 测试同步分析方法 + */ + @Test + public void testAnalyzeDailyReport() { + // 准备测试数据 + Long projectId = 2038890876230889473L; // 使用已存在的项目 ID + + Project project = projectMapper.selectById(projectId); + if (project == null) { + log.warn("测试项目不存在,跳过测试"); + return; + } + + ProjectDailyReport report = new ProjectDailyReport(); + report.setProjectId(projectId); + report.setReportDate(LocalDate.now()); + report.setWorkContent("完成了项目需求分析文档的编写,与团队成员进行了技术讨论"); + report.setTomorrowPlan("开始进行系统架构设计,绘制 UML 图"); + report.setWorkIntensity(3); + report.setNeedHelp(false); + + // 执行分析 + DailyReportAnalysisResult result = analysisService.analyzeDailyReport(projectId, report); + + // 验证结果 + assertNotNull(result, "分析结果不应为空"); + assertEquals(projectId, result.getProjectId(), "项目 ID 应该匹配"); + assertNotNull(result.getOverallProgressAssessment(), "整体进度评估不应为空"); + + log.info("分析结果:{}", result); + } + + /** + * 测试异步分析方法 + */ + @Test + public void testAnalyzeDailyReportAsync() throws InterruptedException { + // 准备测试数据 + Long projectId = 1L; + + Project project = projectMapper.selectById(projectId); + if (project == null) { + log.warn("测试项目不存在,跳过测试"); + return; + } + + ProjectDailyReport report = new ProjectDailyReport(); + report.setProjectId(projectId); + report.setReportDate(LocalDate.now()); + report.setWorkContent("测试工作内容"); + report.setTomorrowPlan("测试明日计划"); + report.setWorkIntensity(4); + report.setNeedHelp(true); + report.setHelpContent("需要技术支持"); + + // 执行异步分析 + analysisService.analyzeDailyReportAsync(projectId, report); + + // 等待异步任务完成 + Thread.sleep(5000); + + log.info("异步分析任务已触发,请检查日志和数据库记录"); + } + + /** + * 测试保存分析结果 + */ + @Test + public void testSaveAnalysisResult() { + // 准备模拟的分析结果 + DailyReportAnalysisResult result = new DailyReportAnalysisResult(); + result.setProjectId(1L); + result.setProjectName("测试项目"); + result.setReportDate(LocalDate.now()); + + // 设置整体进度评估 + DailyReportAnalysisResult.OverallProgressAssessment assessment = + new DailyReportAnalysisResult.OverallProgressAssessment(); + assessment.setStatus("on_track"); + assessment.setDeviationPercentage(BigDecimal.valueOf(5.0)); + assessment.setDescription("项目进度正常,略有提前"); + result.setOverallProgressAssessment(assessment); + + // 设置资源需求 + DailyReportAnalysisResult.ResourceNeed resourceNeed = new DailyReportAnalysisResult.ResourceNeed(); + resourceNeed.setResourceType("human"); + resourceNeed.setResourceName("Java 开发工程师"); + resourceNeed.setQuantity(BigDecimal.valueOf(2)); + resourceNeed.setUnit("人"); + resourceNeed.setReason("项目进度加快,需要增加人力"); + resourceNeed.setSuggestedArrivalDate(LocalDate.now().plusDays(7)); + result.getResourceNeeds().add(resourceNeed); + + // 保存结果 + analysisService.saveAnalysisResult(result); + + log.info("测试分析结果保存完成"); + } +}