feat(ai-analysis): 添加日报 AI 分析功能说明文档及实现

- 新增详尽的日报 AI 分析功能使用说明文档,包含功能概述、接口示例、
  技术细节、错误处理和性能指标
- 添加 AsyncConfig 配置,新增日报 AI 分析任务线程池,支持异步并发处理
- 创建 DailyReportAnalysisResult DTO,定义分析结果数据结构
- 实现 DailyReportAnalysisService 接口,支持异步分析日报并保存分析结果
- 实现 DailyReportAnalysisServiceImpl,集成 AI 分析模型调用和业务数据处理
- 设置 AI 分析系统提示词,规范输出 JSON 结构,确保分析质量和准确性
- 异步执行分析任务,线程池采用 CallerRunsPolicy 拒绝策略保证稳定性
- 设计项目上下文构建逻辑,整合项目信息、里程碑、任务和统计数据为 AI 提示
- 实现分析结果持久化,保存识别风险、资源需求,更新项目进度信息
- 日报 AI 分析任务异步执行异常记录,保证主流程稳定不受影响
This commit is contained in:
2026-03-31 19:22:24 +08:00
parent 5f2aedf57e
commit de2268d9a9
8 changed files with 1251 additions and 358 deletions

View File

@@ -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`

View File

@@ -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();
}
```

View File

@@ -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;
}
}

View File

@@ -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<MilestoneRisk> milestoneRisks;
/**
* 资源需求列表
*/
private List<ResourceNeed> resourceNeeds;
/**
* 进度建议列表
*/
private List<ProgressSuggestion> progressSuggestions;
/**
* 识别的风险列表 (直接入库)
*/
private List<IdentifiedRisk> identifiedRisks;
/**
* 整体进度评估
*/
@Data
public static class OverallProgressAssessment {
/**
* 进度状态ahead-提前on_track-正常delayed-滞后
*/
private String status;
/**
* 进度偏差百分比 (正数表示提前,负数表示滞后)
*/
private BigDecimal deviationPercentage;
/**
* 评估说明
*/
private String description;
/**
* 关键问题
*/
private List<String> 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;
}
}

View File

@@ -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);
}

View File

@@ -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 为 critical60-80 为 high40-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<Long> savedRiskIds = saveIdentifiedRisks(result.getIdentifiedRisks(), projectId, now);
log.info("[日报 AI 分析] 保存 {} 个识别的风险IDs={}", savedRiskIds.size(), savedRiskIds);
}
// 2. 保存资源需求
if (result.getResourceNeeds() != null && !result.getResourceNeeds().isEmpty()) {
List<Long> 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<ProjectMilestone> milestones = projectMilestoneMapper.selectList(
new LambdaQueryWrapper<ProjectMilestone>()
.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<Task> tasks = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.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<Task>()
.eq(Task::getProjectId, projectId)
.eq(Task::getDeleted, 0)
);
long completedTasks = taskMapper.selectCount(
new LambdaQueryWrapper<Task>()
.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<ProjectDailyReport> recentReports = projectDailyReportMapper.selectList(
new LambdaQueryWrapper<ProjectDailyReport>()
.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<Long> saveIdentifiedRisks(List<DailyReportAnalysisResult.IdentifiedRisk> risks,
Long projectId,
LocalDateTime now) {
List<Long> 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<String, Object> 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<Long> saveResourceNeeds(List<DailyReportAnalysisResult.ResourceNeed> resourceNeeds,
Long projectId,
LocalDateTime now) {
List<Long> 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";
};
}
}

View File

@@ -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 "日报同步成功";
}

View File

@@ -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("测试分析结果保存完成");
}
}