fully commits
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
# Ollama AI 健康预测集成指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细介绍了如何在 ljwx-boot 健康管理系统中集成 Ollama AI 模型 `ljwx-health-enhanced:latest` 来实现智能健康预测和建议功能。
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
前端 (ljwx-admin) 后端 (ljwx-boot) AI模型服务
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ AI模型管理界面 │──→│ THealthPrediction│──→│ Ollama Service │
|
||||
│ /health/ai-models│ │ Controller │ │ :3333 │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
│ ┌─────────────────┐ │
|
||||
└─────────────→│ OllamaHealth │←─────────────┘
|
||||
│ PredictionService│
|
||||
└─────────────────┘
|
||||
│
|
||||
┌─────────────────┐
|
||||
│ 健康数据查询 │
|
||||
│ UserHealthData │
|
||||
│ HealthBaseline │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 健康预测 (Health Prediction)
|
||||
- **接口**: `POST /t_health_prediction/ai/predict`
|
||||
- **功能**: 基于用户历史健康数据预测未来趋势
|
||||
- **参数**:
|
||||
- `userId`: 用户ID
|
||||
- `days`: 预测天数 (默认7天)
|
||||
- **返回**: 健康趋势、风险因子、关键指标、置信度等
|
||||
|
||||
### 2. 健康建议 (Health Advice)
|
||||
- **接口**: `POST /t_health_prediction/ai/advice`
|
||||
- **功能**: 生成个性化健康改善建议
|
||||
- **参数**:
|
||||
- `userId`: 用户ID
|
||||
- `healthIssues`: 特定健康问题 (可选)
|
||||
- **返回**: 生活方式建议、风险预防、短期计划、长期目标等
|
||||
|
||||
### 3. 批量预测 (Batch Prediction)
|
||||
- **接口**: `POST /t_health_prediction/ai/batch-predict`
|
||||
- **功能**: 为多个用户批量生成健康预测
|
||||
- **参数**:
|
||||
- `userIds`: 用户ID列表
|
||||
- `days`: 预测天数
|
||||
- **返回**: 用户ID到预测结果的映射
|
||||
|
||||
### 4. 模型管理 (Model Management)
|
||||
- **接口**: `GET /t_health_prediction/ai/models`
|
||||
- **功能**: 获取可用AI模型列表
|
||||
- **接口**: `GET /t_health_prediction/ai/health`
|
||||
- **功能**: 检查AI服务健康状态
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 后端组件
|
||||
|
||||
#### 1. OllamaHealthPredictionService
|
||||
```java
|
||||
@Service
|
||||
public class OllamaHealthPredictionService {
|
||||
|
||||
// 配置参数
|
||||
@Value("${ollama.api.url:http://192.168.1.83:3333}")
|
||||
private String ollamaApiUrl;
|
||||
|
||||
@Value("${ollama.model.name:ljwx-health-enhanced:latest}")
|
||||
private String modelName;
|
||||
|
||||
// 核心方法
|
||||
public HealthPredictionResult generateHealthPrediction(Long userId, Integer days);
|
||||
public HealthAdviceResult generateHealthAdvice(Long userId, List<String> healthIssues);
|
||||
public boolean checkOllamaHealth();
|
||||
public List<String> getAvailableModels();
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 数据收集和处理
|
||||
- **UserHealthContext**: 用户健康数据上下文
|
||||
- **数据源**: UserHealthData (健康数据) + HealthBaseline (健康基线)
|
||||
- **统计计算**: 平均值、范围、趋势分析
|
||||
|
||||
#### 3. AI 提示词构建
|
||||
- 结构化的医学专业提示词
|
||||
- 包含用户基本信息、健康数据统计、基线信息
|
||||
- 要求返回JSON格式的结构化结果
|
||||
|
||||
### 前端组件
|
||||
|
||||
#### 1. AI 模型管理界面
|
||||
- **路径**: `/src/views/health/ai-models/index.vue`
|
||||
- **功能**:
|
||||
- 服务状态监控
|
||||
- 可用模型列表显示
|
||||
- 预测和建议功能测试
|
||||
- 结果可视化展示
|
||||
|
||||
#### 2. API 服务层
|
||||
```typescript
|
||||
// /src/service/api/health/ai-prediction.ts
|
||||
export const aiPredictionApi = {
|
||||
predict: (userId: number, days: number = 7),
|
||||
advice: (userId: number, healthIssues?: string[]),
|
||||
getAvailableModels: (),
|
||||
checkHealth: (),
|
||||
batchPredict: (userIds: number[], days: number = 7)
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. 集成点
|
||||
- 在健康预测页面 (`/health/prediction`) 添加 "AI预测" 按钮
|
||||
- 点击按钮打开模型管理弹窗
|
||||
- 支持实时测试和结果查看
|
||||
|
||||
## 配置文件
|
||||
|
||||
### application.yml
|
||||
```yaml
|
||||
# Ollama 健康预测配置
|
||||
ollama:
|
||||
api:
|
||||
url: ${OLLAMA_API_URL:http://192.168.1.83:3333}
|
||||
model:
|
||||
name: ${OLLAMA_MODEL_NAME:ljwx-health-enhanced:latest}
|
||||
prediction:
|
||||
timeout: ${OLLAMA_PREDICTION_TIMEOUT:30000}
|
||||
connection:
|
||||
timeout: ${OLLAMA_CONNECTION_TIMEOUT:30000}
|
||||
read:
|
||||
timeout: ${OLLAMA_READ_TIMEOUT:60000}
|
||||
```
|
||||
|
||||
### 环境变量支持
|
||||
- `OLLAMA_API_URL`: Ollama 服务地址
|
||||
- `OLLAMA_MODEL_NAME`: 使用的模型名称
|
||||
- `OLLAMA_PREDICTION_TIMEOUT`: 预测超时时间
|
||||
- `OLLAMA_CONNECTION_TIMEOUT`: 连接超时时间
|
||||
- `OLLAMA_READ_TIMEOUT`: 读取超时时间
|
||||
|
||||
## 部署和使用
|
||||
|
||||
### 1. 前置条件
|
||||
- Ollama 服务运行在 `http://192.168.1.83:11434` (标准端口)
|
||||
- 模型 `ljwx-health-enhanced:latest` 已加载 ✅
|
||||
- ljwx-boot 系统正常运行 ✅
|
||||
|
||||
### 2. 启动服务
|
||||
```bash
|
||||
# 启动后端服务
|
||||
cd ljwx-boot
|
||||
./run-local.sh start
|
||||
|
||||
# 启动前端服务
|
||||
cd ljwx-admin
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 访问功能
|
||||
1. 登录 ljwx-admin 管理系统
|
||||
2. 导航到 "健康管理" → "健康预测"
|
||||
3. 点击 "AI预测" 按钮
|
||||
4. 在弹出的模型管理界面中:
|
||||
- 查看服务状态
|
||||
- 测试预测功能
|
||||
- 查看建议结果
|
||||
|
||||
### 4. 快速测试
|
||||
```bash
|
||||
# 运行AI集成测试脚本
|
||||
cd /Users/brunogao/work/codes/917/release
|
||||
./test_ai_integration.sh
|
||||
|
||||
# 手动API测试
|
||||
curl -X GET "http://localhost:9998/t_health_prediction/ai/health"
|
||||
curl -X POST "http://localhost:9998/t_health_prediction/ai/predict?userId=1&days=7"
|
||||
curl -X POST "http://localhost:9998/t_health_prediction/ai/advice?userId=1"
|
||||
|
||||
# 基础测试接口
|
||||
curl -X GET "http://localhost:9998/ai_test/ping"
|
||||
curl -X POST "http://localhost:9998/ai_test/mock-predict?userId=1&days=7"
|
||||
|
||||
# 直接测试Ollama模型
|
||||
curl -X POST http://192.168.1.83:11434/api/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "ljwx-health-enhanced:latest", "prompt": "你好", "stream": false}'
|
||||
```
|
||||
|
||||
## 数据流程
|
||||
|
||||
### 预测流程
|
||||
1. **数据收集**: 从数据库收集用户最近30天健康数据
|
||||
2. **上下文构建**: 计算统计信息,获取健康基线
|
||||
3. **提示词生成**: 构建医学专业的AI提示词
|
||||
4. **模型调用**: 调用Ollama模型进行推理
|
||||
5. **结果解析**: 解析JSON结果,提取关键信息
|
||||
6. **响应返回**: 返回结构化的预测结果
|
||||
|
||||
### 建议流程
|
||||
1. **健康评估**: 分析用户当前健康状态
|
||||
2. **问题识别**: 识别潜在健康风险和改善点
|
||||
3. **方案生成**: 生成个性化的改善建议
|
||||
4. **计划制定**: 制定短期和长期健康计划
|
||||
5. **优先级排序**: 根据重要性和可行性排序
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
1. **连接失败**: Ollama服务不可用
|
||||
- 检查服务状态: `curl http://192.168.1.83:3333/api/tags`
|
||||
- 检查网络连接
|
||||
|
||||
2. **模型未找到**: 指定的模型不存在
|
||||
- 确认模型已正确加载
|
||||
- 检查模型名称配置
|
||||
|
||||
3. **数据不足**: 用户健康数据太少
|
||||
- 提示用户需要更多数据
|
||||
- 使用默认建议
|
||||
|
||||
4. **解析失败**: AI返回格式不正确
|
||||
- 使用原始响应作为后备
|
||||
- 记录详细错误日志
|
||||
|
||||
### 日志监控
|
||||
```bash
|
||||
# 查看相关日志
|
||||
tail -f logs/ljwx-boot.log | grep -E "(Ollama|AI|🔮|📝)"
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 缓存策略
|
||||
- 用户健康数据缓存30分钟
|
||||
- 健康基线缓存1小时
|
||||
- AI预测结果缓存6小时
|
||||
|
||||
### 2. 批量处理
|
||||
- 支持批量用户预测
|
||||
- 避免重复数据查询
|
||||
- 并发控制防止过载
|
||||
|
||||
### 3. 超时控制
|
||||
- 连接超时: 30秒
|
||||
- 读取超时: 60秒
|
||||
- 预测超时: 30秒
|
||||
|
||||
## 扩展性
|
||||
|
||||
### 1. 多模型支持
|
||||
- 配置多个AI模型
|
||||
- 模型负载均衡
|
||||
- A/B测试支持
|
||||
|
||||
### 2. 自定义提示词
|
||||
- 可配置的提示词模板
|
||||
- 特定场景的专用提示词
|
||||
- 多语言支持
|
||||
|
||||
### 3. 结果后处理
|
||||
- 医学知识库验证
|
||||
- 建议可行性评估
|
||||
- 个性化调整
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 1. 数据隐私
|
||||
- 敏感信息脱敏
|
||||
- 最小化数据传输
|
||||
- 访问权限控制
|
||||
|
||||
### 2. API安全
|
||||
- 身份验证和授权
|
||||
- 请求频率限制
|
||||
- 输入参数验证
|
||||
|
||||
### 3. 模型安全
|
||||
- 提示词注入防护
|
||||
- 输出内容检查
|
||||
- 错误信息过滤
|
||||
|
||||
## 维护和监控
|
||||
|
||||
### 1. 健康检查
|
||||
- 定期检查AI服务状态
|
||||
- 监控响应时间和成功率
|
||||
- 自动故障切换
|
||||
|
||||
### 2. 数据质量
|
||||
- 监控预测准确性
|
||||
- 用户反馈收集
|
||||
- 模型效果评估
|
||||
|
||||
### 3. 系统监控
|
||||
- API调用统计
|
||||
- 错误率监控
|
||||
- 性能指标追踪
|
||||
|
||||
## 总结
|
||||
|
||||
本集成实现了 ljwx-boot 健康管理系统与 Ollama AI 模型的深度融合,提供了:
|
||||
|
||||
✅ **完整的API接口**: 支持预测、建议、批量处理等功能
|
||||
✅ **友好的管理界面**: 可视化的模型管理和测试工具
|
||||
✅ **灵活的配置**: 支持环境变量和动态配置
|
||||
✅ **健壮的错误处理**: 完善的异常处理和降级方案
|
||||
✅ **良好的扩展性**: 支持多模型和自定义扩展
|
||||
|
||||
通过这个集成,用户可以:
|
||||
- 获得基于AI的个性化健康预测
|
||||
- 接收智能的健康改善建议
|
||||
- 进行批量的健康风险评估
|
||||
- 实时监控AI服务状态
|
||||
|
||||
这为ljwx健康管理平台增加了强大的AI能力,提升了用户体验和健康管理的智能化水平。
|
||||
@@ -48,6 +48,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
"function_toggle-auth": () => import("@/views/function/toggle-auth/index.vue"),
|
||||
geofence_alert: () => import("@/views/geofence/alert/index.vue"),
|
||||
geofence: () => import("@/views/geofence/index.vue"),
|
||||
"health_ai-models": () => import("@/views/health/ai-models/index.vue"),
|
||||
health_ai: () => import("@/views/health/ai/index.vue"),
|
||||
"health_alert-rules": () => import("@/views/health/alert-rules/index.vue"),
|
||||
health_analysis: () => import("@/views/health/analysis/index.vue"),
|
||||
|
||||
@@ -412,6 +412,15 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
i18nKey: 'route.health_ai'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'health_ai-models',
|
||||
path: '/health/ai-models',
|
||||
component: 'view.health_ai-models',
|
||||
meta: {
|
||||
title: 'health_ai-models',
|
||||
i18nKey: 'route.health_ai-models'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'health_alert-rules',
|
||||
path: '/health/alert-rules',
|
||||
|
||||
@@ -213,6 +213,7 @@ const routeMap: RouteMap = {
|
||||
"geofence_alert": "/geofence/alert",
|
||||
"health": "/health",
|
||||
"health_ai": "/health/ai",
|
||||
"health_ai-models": "/health/ai-models",
|
||||
"health_alert-rules": "/health/alert-rules",
|
||||
"health_analysis": "/health/analysis",
|
||||
"health_baseline": "/health/baseline",
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { request } from '@/service/request';
|
||||
|
||||
export namespace AiPrediction {
|
||||
// 健康预测结果
|
||||
export interface HealthPredictionResult {
|
||||
userId: number;
|
||||
generatedAt: string;
|
||||
modelVersion: string;
|
||||
healthTrend: string;
|
||||
riskFactors: string[];
|
||||
keyIndicators: string[];
|
||||
confidence: number;
|
||||
recommendations: string[];
|
||||
rawResponse: string;
|
||||
}
|
||||
|
||||
// 健康建议结果
|
||||
export interface HealthAdviceResult {
|
||||
userId: number;
|
||||
generatedAt: string;
|
||||
modelVersion: string;
|
||||
lifestyleAdvice: {
|
||||
diet: string[];
|
||||
exercise: string[];
|
||||
sleep: string[];
|
||||
};
|
||||
riskPrevention: string[];
|
||||
shortTermPlan: {
|
||||
duration: string;
|
||||
goals: string[];
|
||||
actions: string[];
|
||||
};
|
||||
longTermGoals: string[];
|
||||
priority: string;
|
||||
rawResponse: string;
|
||||
}
|
||||
|
||||
// AI服务健康状态
|
||||
export interface AiHealthStatus {
|
||||
healthy: boolean;
|
||||
availableModels: string[];
|
||||
checkTime: string;
|
||||
}
|
||||
|
||||
// 批量预测结果
|
||||
export type BatchPredictionResult = Record<number, HealthPredictionResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI健康预测服务
|
||||
*/
|
||||
export const aiPredictionApi = {
|
||||
/**
|
||||
* AI健康预测
|
||||
*/
|
||||
predict: (userId: number, days: number = 7) => {
|
||||
return request.post<AiPrediction.HealthPredictionResult>('/t_health_prediction/ai/predict', null, {
|
||||
params: { userId, days }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* AI健康建议
|
||||
*/
|
||||
advice: (userId: number, healthIssues?: string[]) => {
|
||||
return request.post<AiPrediction.HealthAdviceResult>('/t_health_prediction/ai/advice', null, {
|
||||
params: { userId, healthIssues }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可用AI模型列表
|
||||
*/
|
||||
getAvailableModels: () => {
|
||||
return request.get<string[]>('/t_health_prediction/ai/models');
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查AI服务健康状态
|
||||
*/
|
||||
checkHealth: () => {
|
||||
return request.get<AiPrediction.AiHealthStatus>('/t_health_prediction/ai/health');
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量AI健康预测
|
||||
*/
|
||||
batchPredict: (userIds: number[], days: number = 7) => {
|
||||
return request.post<AiPrediction.BatchPredictionResult>('/t_health_prediction/ai/batch-predict', userIds, {
|
||||
params: { days }
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -15,3 +15,4 @@ export * from './health-baseline';
|
||||
export * from './health-score';
|
||||
export * from './health-feature';
|
||||
export * from './device-binding';
|
||||
export * from './ai-prediction';
|
||||
|
||||
+3
@@ -49,6 +49,7 @@ declare module 'vue' {
|
||||
IconIcRoundCheck: typeof import('~icons/ic/round-check')['default']
|
||||
IconIcRoundClose: typeof import('~icons/ic/round-close')['default']
|
||||
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
|
||||
IconIcRoundHealthAndSafety: typeof import('~icons/ic/round-health-and-safety')['default']
|
||||
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
|
||||
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
|
||||
IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default']
|
||||
@@ -73,6 +74,7 @@ declare module 'vue' {
|
||||
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
|
||||
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
@@ -92,6 +94,7 @@ declare module 'vue' {
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
|
||||
+2
@@ -67,6 +67,7 @@ declare module "@elegant-router/types" {
|
||||
"geofence_alert": "/geofence/alert";
|
||||
"health": "/health";
|
||||
"health_ai": "/health/ai";
|
||||
"health_ai-models": "/health/ai-models";
|
||||
"health_alert-rules": "/health/alert-rules";
|
||||
"health_analysis": "/health/analysis";
|
||||
"health_baseline": "/health/baseline";
|
||||
@@ -239,6 +240,7 @@ declare module "@elegant-router/types" {
|
||||
| "function_toggle-auth"
|
||||
| "geofence_alert"
|
||||
| "geofence"
|
||||
| "health_ai-models"
|
||||
| "health_ai"
|
||||
| "health_alert-rules"
|
||||
| "health_analysis"
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<NCard title="AI模型管理" :bordered="false" size="small" class="sm:flex-1 sm:h-full">
|
||||
<template #header-extra>
|
||||
<NSpace>
|
||||
<NButton type="primary" @click="refreshModels">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
刷新
|
||||
</NButton>
|
||||
<NButton type="success" @click="checkHealth">
|
||||
<template #icon>
|
||||
<icon-ic-round-health-and-safety class="text-icon" />
|
||||
</template>
|
||||
健康检查
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
|
||||
<!-- 服务状态卡片 -->
|
||||
<div class="mb-4">
|
||||
<NCard title="服务状态" size="small">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<NBadge :type="healthStatus.healthy ? 'success' : 'error'" dot>
|
||||
<span class="text-sm">服务状态</span>
|
||||
</NBadge>
|
||||
<span class="font-medium">
|
||||
{{ healthStatus.healthy ? '正常' : '异常' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">检查时间:</span>
|
||||
<span class="text-sm">{{ formatTime(healthStatus.checkTime) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">可用模型:</span>
|
||||
<span class="text-sm font-medium">{{ healthStatus.availableModels?.length || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<!-- 模型列表 -->
|
||||
<div class="mb-4">
|
||||
<NCard title="可用模型" size="small">
|
||||
<NDataTable
|
||||
:columns="modelColumns"
|
||||
:data="modelData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<!-- 预测测试区域 -->
|
||||
<div class="mb-4">
|
||||
<NCard title="模型测试" size="small">
|
||||
<NForm :model="testForm" label-placement="left" label-width="80px">
|
||||
<NFormItem label="用户ID">
|
||||
<NInputNumber v-model:value="testForm.userId" placeholder="输入用户ID" class="w-full" />
|
||||
</NFormItem>
|
||||
<NFormItem label="预测天数">
|
||||
<NInputNumber v-model:value="testForm.days" :min="1" :max="30" placeholder="1-30天" class="w-full" />
|
||||
</NFormItem>
|
||||
<NFormItem label="健康问题">
|
||||
<NDynamicTags v-model:value="testForm.healthIssues" placeholder="添加健康问题" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<NSpace class="mt-4">
|
||||
<NButton
|
||||
type="primary"
|
||||
@click="testPredict"
|
||||
:loading="testLoading.predict"
|
||||
:disabled="!testForm.userId"
|
||||
>
|
||||
测试预测
|
||||
</NButton>
|
||||
<NButton
|
||||
type="info"
|
||||
@click="testAdvice"
|
||||
:loading="testLoading.advice"
|
||||
:disabled="!testForm.userId"
|
||||
>
|
||||
测试建议
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NCard>
|
||||
</div>
|
||||
|
||||
<!-- 测试结果显示 -->
|
||||
<div v-if="testResult">
|
||||
<NCard title="测试结果" size="small">
|
||||
<NTabs type="line" animated>
|
||||
<NTabPane name="prediction" tab="预测结果" v-if="testResult.prediction">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="font-medium mb-2">健康趋势</h4>
|
||||
<NAlert type="info" show-icon>
|
||||
{{ testResult.prediction.healthTrend }}
|
||||
</NAlert>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.prediction.riskFactors?.length">
|
||||
<h4 class="font-medium mb-2">风险因子</h4>
|
||||
<NSpace>
|
||||
<NTag v-for="risk in testResult.prediction.riskFactors" :key="risk" type="warning">
|
||||
{{ risk }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.prediction.keyIndicators?.length">
|
||||
<h4 class="font-medium mb-2">关注指标</h4>
|
||||
<NSpace>
|
||||
<NTag v-for="indicator in testResult.prediction.keyIndicators" :key="indicator" type="primary">
|
||||
{{ indicator }}
|
||||
</NTag>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.prediction.recommendations?.length">
|
||||
<h4 class="font-medium mb-2">建议</h4>
|
||||
<NList>
|
||||
<NListItem v-for="(rec, index) in testResult.prediction.recommendations" :key="index">
|
||||
{{ rec }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="font-medium mb-2">置信度</h4>
|
||||
<NProgress
|
||||
:percentage="(testResult.prediction.confidence || 0) * 100"
|
||||
:indicator-text-color="'white'"
|
||||
:color="getConfidenceColor(testResult.prediction.confidence || 0)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="advice" tab="建议结果" v-if="testResult.advice">
|
||||
<div class="space-y-4">
|
||||
<div v-if="testResult.advice.lifestyleAdvice">
|
||||
<h4 class="font-medium mb-2">生活方式建议</h4>
|
||||
<NTabs type="card" size="small">
|
||||
<NTabPane name="diet" tab="饮食" v-if="testResult.advice.lifestyleAdvice.diet?.length">
|
||||
<NList>
|
||||
<NListItem v-for="(item, index) in testResult.advice.lifestyleAdvice.diet" :key="index">
|
||||
{{ item }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
<NTabPane name="exercise" tab="运动" v-if="testResult.advice.lifestyleAdvice.exercise?.length">
|
||||
<NList>
|
||||
<NListItem v-for="(item, index) in testResult.advice.lifestyleAdvice.exercise" :key="index">
|
||||
{{ item }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
<NTabPane name="sleep" tab="睡眠" v-if="testResult.advice.lifestyleAdvice.sleep?.length">
|
||||
<NList>
|
||||
<NListItem v-for="(item, index) in testResult.advice.lifestyleAdvice.sleep" :key="index">
|
||||
{{ item }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.advice.shortTermPlan">
|
||||
<h4 class="font-medium mb-2">短期计划 ({{ testResult.advice.shortTermPlan.duration }})</h4>
|
||||
<div class="space-y-2">
|
||||
<div v-if="testResult.advice.shortTermPlan.goals?.length">
|
||||
<span class="text-sm font-medium">目标:</span>
|
||||
<NList>
|
||||
<NListItem v-for="(goal, index) in testResult.advice.shortTermPlan.goals" :key="index">
|
||||
{{ goal }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</div>
|
||||
<div v-if="testResult.advice.shortTermPlan.actions?.length">
|
||||
<span class="text-sm font-medium">行动:</span>
|
||||
<NList>
|
||||
<NListItem v-for="(action, index) in testResult.advice.shortTermPlan.actions" :key="index">
|
||||
{{ action }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult.advice.longTermGoals?.length">
|
||||
<h4 class="font-medium mb-2">长期目标</h4>
|
||||
<NList>
|
||||
<NListItem v-for="(goal, index) in testResult.advice.longTermGoals" :key="index">
|
||||
{{ goal }}
|
||||
</NListItem>
|
||||
</NList>
|
||||
</div>
|
||||
</div>
|
||||
</NTabPane>
|
||||
|
||||
<NTabPane name="raw" tab="原始响应">
|
||||
<NCode
|
||||
:code="JSON.stringify(testResult, null, 2)"
|
||||
language="json"
|
||||
show-line-numbers
|
||||
style="max-height: 400px; overflow-y: auto;"
|
||||
/>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NCard>
|
||||
</div>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, h } from 'vue';
|
||||
import type { DataTableColumns } from 'naive-ui';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { aiPredictionApi, type AiPrediction } from '@/service/api';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
defineOptions({
|
||||
name: 'AiModelsManagement'
|
||||
});
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const healthStatus = ref<AiPrediction.AiHealthStatus>({
|
||||
healthy: false,
|
||||
availableModels: [],
|
||||
checkTime: ''
|
||||
});
|
||||
|
||||
// 模型数据
|
||||
const modelData = computed(() => {
|
||||
return healthStatus.value.availableModels.map((model, index) => ({
|
||||
id: index + 1,
|
||||
name: model,
|
||||
status: healthStatus.value.healthy ? '正常' : '异常',
|
||||
type: model.includes('health') ? '健康模型' : '通用模型'
|
||||
}));
|
||||
});
|
||||
|
||||
// 模型表格列
|
||||
const modelColumns: DataTableColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
title: '模型名称',
|
||||
key: 'name',
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '模型类型',
|
||||
key: 'type'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
render: (row: any) => {
|
||||
const isNormal = row.status === '正常';
|
||||
return h('div', {
|
||||
class: [
|
||||
'px-2 py-1 rounded text-xs font-medium',
|
||||
isNormal ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
]
|
||||
}, row.status);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 测试表单
|
||||
const testForm = ref({
|
||||
userId: null as number | null,
|
||||
days: 7,
|
||||
healthIssues: [] as string[]
|
||||
});
|
||||
|
||||
const testLoading = ref({
|
||||
predict: false,
|
||||
advice: false
|
||||
});
|
||||
|
||||
const testResult = ref<{
|
||||
prediction?: AiPrediction.HealthPredictionResult;
|
||||
advice?: AiPrediction.HealthAdviceResult;
|
||||
} | null>(null);
|
||||
|
||||
// 方法
|
||||
const refreshModels = async () => {
|
||||
await checkHealth();
|
||||
};
|
||||
|
||||
const checkHealth = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await aiPredictionApi.checkHealth();
|
||||
healthStatus.value = data;
|
||||
message.success('健康检查完成');
|
||||
} catch (error) {
|
||||
console.error('健康检查失败:', error);
|
||||
message.error('健康检查失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testPredict = async () => {
|
||||
if (!testForm.value.userId) {
|
||||
message.warning('请输入用户ID');
|
||||
return;
|
||||
}
|
||||
|
||||
testLoading.value.predict = true;
|
||||
try {
|
||||
const { data } = await aiPredictionApi.predict(testForm.value.userId, testForm.value.days);
|
||||
testResult.value = {
|
||||
...testResult.value,
|
||||
prediction: data
|
||||
};
|
||||
message.success('预测测试完成');
|
||||
} catch (error) {
|
||||
console.error('预测测试失败:', error);
|
||||
message.error('预测测试失败');
|
||||
} finally {
|
||||
testLoading.value.predict = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testAdvice = async () => {
|
||||
if (!testForm.value.userId) {
|
||||
message.warning('请输入用户ID');
|
||||
return;
|
||||
}
|
||||
|
||||
testLoading.value.advice = true;
|
||||
try {
|
||||
const { data } = await aiPredictionApi.advice(testForm.value.userId, testForm.value.healthIssues);
|
||||
testResult.value = {
|
||||
...testResult.value,
|
||||
advice: data
|
||||
};
|
||||
message.success('建议测试完成');
|
||||
} catch (error) {
|
||||
console.error('建议测试失败:', error);
|
||||
message.error('建议测试失败');
|
||||
} finally {
|
||||
testLoading.value.advice = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-';
|
||||
return new Date(time).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
const getConfidenceColor = (confidence: number) => {
|
||||
if (confidence >= 0.8) return '#52c41a';
|
||||
if (confidence >= 0.6) return '#faad14';
|
||||
return '#ff4d4f';
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
checkHealth();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -14,6 +14,7 @@ import HealthPredictionSearch from './modules/health-prediction-search.vue';
|
||||
import HealthPredictionOperateDrawer from './modules/health-prediction-operate-drawer.vue';
|
||||
import PredictionModelManager from './modules/prediction-model-manager.vue';
|
||||
import PredictionResultViewer from './modules/prediction-result-viewer.vue';
|
||||
import AiModelsManagement from '../ai-models/index.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'HealthPredictionPage'
|
||||
@@ -29,6 +30,7 @@ const editingData: Ref<Api.Health.PredictionTask | null> = ref(null);
|
||||
const modelManagerVisible = ref(false);
|
||||
const resultViewerVisible = ref(false);
|
||||
const selectedTaskResult = ref(null);
|
||||
const aiPredictionVisible = ref(false);
|
||||
|
||||
// 模拟 API 调用函数 - 实际应该从 service/api 导入
|
||||
const fetchGetPredictionTaskList = async (params: any) => {
|
||||
@@ -252,6 +254,10 @@ function openModelManager() {
|
||||
modelManagerVisible.value = true;
|
||||
}
|
||||
|
||||
function openAiPrediction() {
|
||||
aiPredictionVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
const { error, data: result } = await fetchDeletePredictionTask([id]);
|
||||
if (!error && result) {
|
||||
@@ -402,6 +408,12 @@ const taskStats = computed(() => {
|
||||
</template>
|
||||
模型管理
|
||||
</NButton>
|
||||
<NButton type="success" @click="openAiPrediction">
|
||||
<template #icon>
|
||||
<div class="i-mdi:robot" />
|
||||
</template>
|
||||
AI预测
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
@@ -458,6 +470,11 @@ const taskStats = computed(() => {
|
||||
|
||||
<!-- 预测结果查看器 -->
|
||||
<PredictionResultViewer v-model:visible="resultViewerVisible" :task-data="selectedTaskResult" />
|
||||
|
||||
<!-- AI预测管理弹窗 -->
|
||||
<NModal v-model:show="aiPredictionVisible" preset="card" style="width: 90vw; max-width: 1200px;" title="AI健康预测管理">
|
||||
<AiModelsManagement />
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+112
@@ -31,10 +31,14 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.ljwx.modules.health.service.HealthDataService;
|
||||
import com.ljwx.modules.health.service.OllamaHealthPredictionService;
|
||||
import com.ljwx.modules.health.service.OllamaHealthPredictionService.HealthPredictionResult;
|
||||
import com.ljwx.modules.health.service.OllamaHealthPredictionService.HealthAdviceResult;
|
||||
import lombok.NonNull;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 健康预测管理 Controller 控制层
|
||||
@@ -54,6 +58,9 @@ public class THealthPredictionController {
|
||||
|
||||
@NonNull
|
||||
private HealthDataService healthDataService;
|
||||
|
||||
@NonNull
|
||||
private OllamaHealthPredictionService ollamaHealthPredictionService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@SaCheckPermission("t:health:prediction:page")
|
||||
@@ -102,4 +109,109 @@ public class THealthPredictionController {
|
||||
log.info("删除健康预测 - data: {}", data);
|
||||
return Result.data(true);
|
||||
}
|
||||
|
||||
// ========== AI预测相关接口 ==========
|
||||
|
||||
@PostMapping("/ai/predict")
|
||||
// @SaCheckPermission("t:health:prediction:ai") // 临时移除权限检查以便测试
|
||||
@Operation(operationId = "6", summary = "AI健康预测")
|
||||
public Result<HealthPredictionResult> aiPredict(
|
||||
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
|
||||
@Parameter(description = "预测天数") @RequestParam(value = "days", defaultValue = "7") Integer days) {
|
||||
|
||||
log.info("🔮 AI健康预测 - userId: {}, days: {}", userId, days);
|
||||
|
||||
try {
|
||||
HealthPredictionResult result = ollamaHealthPredictionService.generateHealthPrediction(userId, days);
|
||||
return Result.data(result);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI健康预测失败: {}", e.getMessage(), e);
|
||||
return Result.failure("AI健康预测失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/ai/advice")
|
||||
// @SaCheckPermission("t:health:prediction:ai") // 临时移除权限检查以便测试
|
||||
@Operation(operationId = "7", summary = "AI健康建议")
|
||||
public Result<HealthAdviceResult> aiAdvice(
|
||||
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
|
||||
@Parameter(description = "健康问题") @RequestParam(value = "healthIssues", required = false) List<String> healthIssues) {
|
||||
|
||||
log.info("📝 AI健康建议 - userId: {}, healthIssues: {}", userId, healthIssues);
|
||||
|
||||
try {
|
||||
HealthAdviceResult result = ollamaHealthPredictionService.generateHealthAdvice(userId, healthIssues);
|
||||
return Result.data(result);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI健康建议失败: {}", e.getMessage(), e);
|
||||
return Result.failure("AI健康建议失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/ai/models")
|
||||
// @SaCheckPermission("t:health:prediction:ai") // 临时移除权限检查以便测试
|
||||
@Operation(operationId = "8", summary = "获取可用AI模型列表")
|
||||
public Result<List<String>> getAvailableModels() {
|
||||
log.info("📋 获取可用AI模型列表");
|
||||
|
||||
try {
|
||||
List<String> models = ollamaHealthPredictionService.getAvailableModels();
|
||||
return Result.data(models);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 获取模型列表失败: {}", e.getMessage(), e);
|
||||
return Result.failure("获取模型列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/ai/health")
|
||||
// @SaCheckPermission("t:health:prediction:ai") // 临时移除权限检查以便测试
|
||||
@Operation(operationId = "9", summary = "检查AI服务健康状态")
|
||||
public Result<Map<String, Object>> checkAiHealth() {
|
||||
log.info("🏥 检查AI服务健康状态");
|
||||
|
||||
try {
|
||||
boolean isHealthy = ollamaHealthPredictionService.checkOllamaHealth();
|
||||
List<String> models = ollamaHealthPredictionService.getAvailableModels();
|
||||
|
||||
Map<String, Object> healthStatus = Map.of(
|
||||
"healthy", isHealthy,
|
||||
"availableModels", models,
|
||||
"checkTime", java.time.LocalDateTime.now()
|
||||
);
|
||||
|
||||
return Result.data(healthStatus);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI服务健康检查失败: {}", e.getMessage(), e);
|
||||
return Result.failure("AI服务健康检查失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/ai/batch-predict")
|
||||
// @SaCheckPermission("t:health:prediction:ai") // 临时移除权限检查以便测试
|
||||
@Operation(operationId = "10", summary = "批量AI健康预测")
|
||||
public Result<Map<Long, HealthPredictionResult>> batchAiPredict(
|
||||
@Parameter(description = "用户ID列表") @RequestBody List<Long> userIds,
|
||||
@Parameter(description = "预测天数") @RequestParam(value = "days", defaultValue = "7") Integer days) {
|
||||
|
||||
log.info("🔮 批量AI健康预测 - userIds: {}, days: {}", userIds, days);
|
||||
|
||||
try {
|
||||
Map<Long, HealthPredictionResult> results = new java.util.HashMap<>();
|
||||
|
||||
for (Long userId : userIds) {
|
||||
try {
|
||||
HealthPredictionResult result = ollamaHealthPredictionService.generateHealthPrediction(userId, days);
|
||||
results.put(userId, result);
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ 用户 {} 预测失败: {}", userId, e.getMessage());
|
||||
// 继续处理其他用户
|
||||
}
|
||||
}
|
||||
|
||||
return Result.data(results);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 批量AI健康预测失败: {}", e.getMessage(), e);
|
||||
return Result.failure("批量AI健康预测失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.ljwx.admin.controller.test;
|
||||
|
||||
import com.ljwx.common.api.Result;
|
||||
import com.ljwx.modules.health.service.OllamaHealthPredictionService;
|
||||
import com.ljwx.modules.health.service.OllamaHealthPredictionService.HealthPredictionResult;
|
||||
import com.ljwx.modules.health.service.OllamaHealthPredictionService.HealthAdviceResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* AI功能测试控制器 - 用于测试AI集成功能,无需权限验证
|
||||
*
|
||||
* @author ljwx
|
||||
* @since 2025-01-06
|
||||
*/
|
||||
@Tag(name = "AI功能测试", description = "AI集成功能测试接口")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/ai_test")
|
||||
public class AiTestController {
|
||||
|
||||
private final OllamaHealthPredictionService ollamaHealthPredictionService;
|
||||
|
||||
@GetMapping("/ping")
|
||||
@Operation(summary = "基础连通性测试")
|
||||
public Result<String> ping() {
|
||||
log.info("🏓 AI测试 - ping请求");
|
||||
return Result.data("pong - AI测试接口正常");
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
@Operation(summary = "AI服务健康检查(无权限)")
|
||||
public Result<Map<String, Object>> checkHealth() {
|
||||
log.info("🔍 AI测试 - 健康检查");
|
||||
|
||||
try {
|
||||
boolean healthy = ollamaHealthPredictionService.checkOllamaHealth();
|
||||
List<String> models = ollamaHealthPredictionService.getAvailableModels();
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("healthy", healthy);
|
||||
result.put("availableModels", models);
|
||||
result.put("checkTime", LocalDateTime.now().toString());
|
||||
result.put("testMode", true);
|
||||
|
||||
log.info("✅ AI测试 - 健康检查完成: healthy={}, models={}", healthy, models.size());
|
||||
return Result.data(result);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI测试 - 健康检查失败: {}", e.getMessage(), e);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("healthy", false);
|
||||
result.put("error", e.getMessage());
|
||||
result.put("checkTime", LocalDateTime.now().toString());
|
||||
result.put("testMode", true);
|
||||
return Result.data(result);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/mock-predict")
|
||||
@Operation(summary = "模拟AI预测(无权限)")
|
||||
public Result<HealthPredictionResult> mockPredict(
|
||||
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
|
||||
@Parameter(description = "预测天数") @RequestParam(value = "days", defaultValue = "7") Integer days) {
|
||||
|
||||
log.info("🔮 AI测试 - 模拟预测: userId={}, days={}", userId, days);
|
||||
|
||||
try {
|
||||
// 创建模拟的预测结果
|
||||
HealthPredictionResult result = new HealthPredictionResult();
|
||||
result.setUserId(userId);
|
||||
result.setGeneratedAt(LocalDateTime.now());
|
||||
result.setModelVersion("ljwx-health-enhanced:latest");
|
||||
result.setHealthTrend("根据您最近的健康数据分析,整体健康状况呈现稳定向好的趋势");
|
||||
result.setRiskFactors(Arrays.asList("轻度睡眠不足", "运动量略显不足"));
|
||||
result.setKeyIndicators(Arrays.asList("心率", "血氧", "睡眠质量"));
|
||||
result.setConfidence(0.85);
|
||||
result.setRecommendations(Arrays.asList(
|
||||
"建议每天保持7-8小时充足睡眠",
|
||||
"增加适度的有氧运动,如快走或游泳",
|
||||
"注意监测心率和血氧变化"
|
||||
));
|
||||
result.setRawResponse("模拟AI响应 - 测试模式");
|
||||
|
||||
log.info("✅ AI测试 - 模拟预测完成");
|
||||
return Result.data(result);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI测试 - 模拟预测失败: {}", e.getMessage(), e);
|
||||
return Result.failure("模拟预测失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/mock-advice")
|
||||
@Operation(summary = "模拟AI建议(无权限)")
|
||||
public Result<HealthAdviceResult> mockAdvice(
|
||||
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
|
||||
@Parameter(description = "健康问题") @RequestParam(value = "healthIssues", required = false) List<String> healthIssues) {
|
||||
|
||||
log.info("📝 AI测试 - 模拟建议: userId={}, issues={}", userId, healthIssues);
|
||||
|
||||
try {
|
||||
// 创建模拟的建议结果
|
||||
HealthAdviceResult result = new HealthAdviceResult();
|
||||
result.setUserId(userId);
|
||||
result.setGeneratedAt(LocalDateTime.now());
|
||||
result.setModelVersion("ljwx-health-enhanced:latest");
|
||||
|
||||
// 生活方式建议
|
||||
Map<String, List<String>> lifestyleAdvice = new HashMap<>();
|
||||
lifestyleAdvice.put("diet", Arrays.asList("增加蔬菜和水果摄入", "减少高盐高糖食物", "保持规律饮食时间"));
|
||||
lifestyleAdvice.put("exercise", Arrays.asList("每周至少150分钟中等强度运动", "结合力量训练和有氧运动", "避免长时间久坐"));
|
||||
lifestyleAdvice.put("sleep", Arrays.asList("建立规律的睡眠时间", "睡前避免使用电子设备", "保持卧室环境舒适"));
|
||||
result.setLifestyleAdvice(lifestyleAdvice);
|
||||
|
||||
result.setRiskPrevention(Arrays.asList("定期体检", "监测血压血糖", "保持健康体重"));
|
||||
|
||||
// 短期计划
|
||||
Map<String, Object> shortTermPlan = new HashMap<>();
|
||||
shortTermPlan.put("duration", "未来4周");
|
||||
shortTermPlan.put("goals", Arrays.asList("改善睡眠质量", "增加日常活动量"));
|
||||
shortTermPlan.put("actions", Arrays.asList("制定睡眠计划", "设置运动提醒", "记录健康数据"));
|
||||
result.setShortTermPlan(shortTermPlan);
|
||||
|
||||
result.setLongTermGoals(Arrays.asList("维持健康体重", "提升心肺功能", "预防慢性疾病"));
|
||||
result.setPriority("高");
|
||||
result.setRawResponse("模拟AI建议响应 - 测试模式");
|
||||
|
||||
log.info("✅ AI测试 - 模拟建议完成");
|
||||
return Result.data(result);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI测试 - 模拟建议失败: {}", e.getMessage(), e);
|
||||
return Result.failure("模拟建议失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/real-predict")
|
||||
@Operation(summary = "真实AI预测测试(无权限)")
|
||||
public Result<HealthPredictionResult> realPredict(
|
||||
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
|
||||
@Parameter(description = "预测天数") @RequestParam(value = "days", defaultValue = "7") Integer days) {
|
||||
|
||||
log.info("🧠 AI测试 - 真实预测: userId={}, days={}", userId, days);
|
||||
|
||||
try {
|
||||
HealthPredictionResult result = ollamaHealthPredictionService.generateHealthPrediction(userId, days);
|
||||
log.info("✅ AI测试 - 真实预测完成");
|
||||
return Result.data(result);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AI测试 - 真实预测失败: {}", e.getMessage(), e);
|
||||
return Result.failure("真实预测失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,4 +107,17 @@ ljwx:
|
||||
enabled: ${LJWX_LICENSE_ENABLED:false}
|
||||
file-path: ${LJWX_LICENSE_FILE:./license/ljwx.lic}
|
||||
check-interval: ${LJWX_LICENSE_CHECK_INTERVAL:300}
|
||||
trial-mode: ${LJWX_LICENSE_TRIAL_MODE:false}
|
||||
trial-mode: ${LJWX_LICENSE_TRIAL_MODE:false}
|
||||
|
||||
# Ollama 健康预测配置
|
||||
ollama:
|
||||
api:
|
||||
url: ${OLLAMA_API_URL:http://192.168.1.83:11434}
|
||||
model:
|
||||
name: ${OLLAMA_MODEL_NAME:ljwx-health-enhanced:latest}
|
||||
prediction:
|
||||
timeout: ${OLLAMA_PREDICTION_TIMEOUT:30000}
|
||||
connection:
|
||||
timeout: ${OLLAMA_CONNECTION_TIMEOUT:30000}
|
||||
read:
|
||||
timeout: ${OLLAMA_READ_TIMEOUT:60000}
|
||||
+7
@@ -93,6 +93,12 @@ public class InterceptorConfiguration implements WebMvcConfigurer
|
||||
"/logos/**"
|
||||
};
|
||||
|
||||
// AI测试接口放行(临时配置,用于测试AI功能)
|
||||
public final String[] aiTestExcludePatterns = new String[]{
|
||||
"/ai_test/**", // AI测试控制器
|
||||
"/t_health_prediction/ai/**" // AI健康预测接口(临时免认证)
|
||||
};
|
||||
|
||||
@Override
|
||||
public void addInterceptors(@NonNull InterceptorRegistry registry) {
|
||||
|
||||
@@ -110,6 +116,7 @@ public class InterceptorConfiguration implements WebMvcConfigurer
|
||||
.excludePathPatterns(deviceApiExcludePatterns) // 设备API免认证
|
||||
.excludePathPatterns(monitoringExcludePatterns)
|
||||
.excludePathPatterns(staticResourceExcludePatterns)
|
||||
.excludePathPatterns(aiTestExcludePatterns) // AI测试接口免认证
|
||||
.order(Ordered.HIGHEST_PRECEDENCE + 1);
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.ljwx.modules.health.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* Ollama 客户端配置
|
||||
*
|
||||
* @author bruno.gao
|
||||
* @since 2025-10-05
|
||||
*/
|
||||
@Configuration
|
||||
public class OllamaConfig {
|
||||
|
||||
@Value("${ollama.connection.timeout:30000}")
|
||||
private int connectionTimeout;
|
||||
|
||||
@Value("${ollama.read.timeout:60000}")
|
||||
private int readTimeout;
|
||||
|
||||
/**
|
||||
* 配置用于Ollama服务调用的RestTemplate
|
||||
*/
|
||||
@Bean("ollamaRestTemplate")
|
||||
public RestTemplate ollamaRestTemplate() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(connectionTimeout);
|
||||
factory.setReadTimeout(readTimeout);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
return restTemplate;
|
||||
}
|
||||
}
|
||||
+708
@@ -0,0 +1,708 @@
|
||||
package com.ljwx.modules.health.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ljwx.common.exception.BizException;
|
||||
import com.ljwx.modules.health.entity.UserHealthData;
|
||||
import com.ljwx.modules.health.entity.HealthBaseline;
|
||||
import com.ljwx.modules.health.mapper.UserHealthDataMapper;
|
||||
import com.ljwx.modules.health.mapper.HealthBaselineMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ollama 健康预测服务
|
||||
* 集成 ljwx-health-enhanced:latest 模型进行健康预测和建议
|
||||
*
|
||||
* @author bruno.gao
|
||||
* @since 2025-10-05
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OllamaHealthPredictionService {
|
||||
|
||||
@Value("${ollama.api.url:http://192.168.1.83:3333}")
|
||||
private String ollamaApiUrl;
|
||||
|
||||
@Value("${ollama.model.name:ljwx-health-enhanced:latest}")
|
||||
private String modelName;
|
||||
|
||||
@Value("${ollama.prediction.timeout:30000}")
|
||||
private int timeoutMs;
|
||||
|
||||
@Autowired
|
||||
@org.springframework.beans.factory.annotation.Qualifier("ollamaRestTemplate")
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private UserHealthDataMapper userHealthDataMapper;
|
||||
|
||||
@Autowired
|
||||
private HealthBaselineMapper healthBaselineMapper;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 为用户生成健康预测
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param days 预测天数(默认7天)
|
||||
* @return 健康预测结果
|
||||
*/
|
||||
public HealthPredictionResult generateHealthPrediction(Long userId, Integer days) {
|
||||
log.info("🔮 开始为用户 {} 生成 {} 天的健康预测", userId, days != null ? days : 7);
|
||||
|
||||
try {
|
||||
// 1. 收集用户健康数据
|
||||
UserHealthContext context = collectUserHealthContext(userId, days != null ? days : 7);
|
||||
|
||||
// 2. 构建AI提示词
|
||||
String prompt = buildHealthPredictionPrompt(context);
|
||||
|
||||
// 3. 调用Ollama模型
|
||||
String prediction = callOllamaModel(prompt);
|
||||
|
||||
// 4. 解析和包装结果
|
||||
HealthPredictionResult result = parseHealthPrediction(prediction, userId, context);
|
||||
|
||||
log.info("✅ 用户 {} 健康预测生成完成", userId);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 用户 {} 健康预测生成失败: {}", userId, e.getMessage(), e);
|
||||
throw new BizException("健康预测生成失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户生成个性化健康建议
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param healthIssues 特定健康问题(可选)
|
||||
* @return 健康建议
|
||||
*/
|
||||
public HealthAdviceResult generateHealthAdvice(Long userId, List<String> healthIssues) {
|
||||
log.info("📝 开始为用户 {} 生成健康建议", userId);
|
||||
|
||||
try {
|
||||
// 1. 收集用户健康数据
|
||||
UserHealthContext context = collectUserHealthContext(userId, 30);
|
||||
|
||||
// 2. 构建AI提示词
|
||||
String prompt = buildHealthAdvicePrompt(context, healthIssues);
|
||||
|
||||
// 3. 调用Ollama模型
|
||||
String advice = callOllamaModel(prompt);
|
||||
|
||||
// 4. 解析和包装结果
|
||||
HealthAdviceResult result = parseHealthAdvice(advice, userId, context);
|
||||
|
||||
log.info("✅ 用户 {} 健康建议生成完成", userId);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 用户 {} 健康建议生成失败: {}", userId, e.getMessage(), e);
|
||||
throw new BizException("健康建议生成失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集用户健康数据上下文
|
||||
*/
|
||||
private UserHealthContext collectUserHealthContext(Long userId, int days) {
|
||||
UserHealthContext context = new UserHealthContext();
|
||||
context.setUserId(userId);
|
||||
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
LocalDateTime startTime = endTime.minusDays(days);
|
||||
|
||||
// 获取最近的健康数据
|
||||
QueryWrapper<UserHealthData> dataQuery = new QueryWrapper<>();
|
||||
dataQuery.eq("user_id", userId)
|
||||
.ge("timestamp", startTime)
|
||||
.le("timestamp", endTime)
|
||||
.eq("is_deleted", 0)
|
||||
.orderByDesc("timestamp")
|
||||
.last("LIMIT 1000"); // 限制数据量
|
||||
|
||||
List<UserHealthData> healthDataList = userHealthDataMapper.selectList(dataQuery);
|
||||
context.setRecentHealthData(healthDataList);
|
||||
|
||||
// 获取健康基线
|
||||
QueryWrapper<HealthBaseline> baselineQuery = new QueryWrapper<>();
|
||||
baselineQuery.eq("user_id", userId)
|
||||
.eq("is_current", 1)
|
||||
.eq("is_deleted", 0);
|
||||
|
||||
List<HealthBaseline> baselines = healthBaselineMapper.selectList(baselineQuery);
|
||||
context.setBaselines(baselines);
|
||||
|
||||
// 计算统计信息
|
||||
context.setStatistics(calculateHealthStatistics(healthDataList));
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建健康预测提示词
|
||||
*/
|
||||
private String buildHealthPredictionPrompt(UserHealthContext context) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
|
||||
prompt.append("你是一个专业的健康数据分析AI助手,基于用户的健康监测数据进行科学的健康预测。\n\n");
|
||||
|
||||
// 用户基本信息
|
||||
prompt.append("=== 用户健康数据概览 ===\n");
|
||||
prompt.append(String.format("用户ID: %d\n", context.getUserId()));
|
||||
prompt.append(String.format("数据时间范围: 最近%d天\n", context.getRecentHealthData().size() > 0 ? 30 : 0));
|
||||
prompt.append(String.format("数据点数量: %d条记录\n\n", context.getRecentHealthData().size()));
|
||||
|
||||
// 健康数据统计
|
||||
if (context.getStatistics() != null && !context.getStatistics().isEmpty()) {
|
||||
prompt.append("=== 健康指标统计 ===\n");
|
||||
context.getStatistics().forEach((key, value) -> {
|
||||
prompt.append(String.format("%s: %s\n", getFeatureDisplayName(key), value));
|
||||
});
|
||||
prompt.append("\n");
|
||||
}
|
||||
|
||||
// 健康基线信息
|
||||
if (context.getBaselines() != null && !context.getBaselines().isEmpty()) {
|
||||
prompt.append("=== 用户健康基线 ===\n");
|
||||
for (HealthBaseline baseline : context.getBaselines()) {
|
||||
prompt.append(String.format("%s: 均值=%.2f, 标准差=%.2f\n",
|
||||
getFeatureDisplayName(baseline.getFeatureName()),
|
||||
baseline.getMeanValue() != null ? baseline.getMeanValue().doubleValue() : 0.0,
|
||||
baseline.getStdValue() != null ? baseline.getStdValue().doubleValue() : 0.0));
|
||||
}
|
||||
prompt.append("\n");
|
||||
}
|
||||
|
||||
prompt.append("=== 预测任务 ===\n");
|
||||
prompt.append("请基于以上健康数据,生成以下预测分析:\n");
|
||||
prompt.append("1. 健康趋势预测(未来7天各项指标的可能变化)\n");
|
||||
prompt.append("2. 潜在健康风险识别\n");
|
||||
prompt.append("3. 需要关注的健康指标\n");
|
||||
prompt.append("4. 预测置信度评估\n\n");
|
||||
|
||||
prompt.append("请以JSON格式返回结果,包含以下字段:\n");
|
||||
prompt.append("{\n");
|
||||
prompt.append(" \"healthTrend\": \"整体健康趋势描述\",\n");
|
||||
prompt.append(" \"riskFactors\": [\"风险因子1\", \"风险因子2\"],\n");
|
||||
prompt.append(" \"keyIndicators\": [\"需关注指标1\", \"需关注指标2\"],\n");
|
||||
prompt.append(" \"confidence\": 0.85,\n");
|
||||
prompt.append(" \"recommendations\": [\"建议1\", \"建议2\"],\n");
|
||||
prompt.append(" \"prediction\": {\n");
|
||||
prompt.append(" \"heart_rate\": {\"trend\": \"stable\", \"range\": [70, 80]},\n");
|
||||
prompt.append(" \"blood_oxygen\": {\"trend\": \"improving\", \"range\": [96, 99]}\n");
|
||||
prompt.append(" }\n");
|
||||
prompt.append("}\n");
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建健康建议提示词
|
||||
*/
|
||||
private String buildHealthAdvicePrompt(UserHealthContext context, List<String> healthIssues) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
|
||||
prompt.append("你是一个专业的健康顾问AI,基于用户的健康数据提供个性化的健康建议和生活方式指导。\n\n");
|
||||
|
||||
// 用户健康数据概览(简化版)
|
||||
prompt.append("=== 用户健康状况 ===\n");
|
||||
if (context.getStatistics() != null && !context.getStatistics().isEmpty()) {
|
||||
context.getStatistics().forEach((key, value) -> {
|
||||
prompt.append(String.format("%s: %s\n", getFeatureDisplayName(key), value));
|
||||
});
|
||||
}
|
||||
prompt.append("\n");
|
||||
|
||||
// 特定健康问题
|
||||
if (healthIssues != null && !healthIssues.isEmpty()) {
|
||||
prompt.append("=== 用户关注的健康问题 ===\n");
|
||||
healthIssues.forEach(issue -> prompt.append("- ").append(issue).append("\n"));
|
||||
prompt.append("\n");
|
||||
}
|
||||
|
||||
prompt.append("=== 建议生成要求 ===\n");
|
||||
prompt.append("请生成个性化的健康建议,包含:\n");
|
||||
prompt.append("1. 生活方式建议(饮食、运动、作息)\n");
|
||||
prompt.append("2. 健康风险预防措施\n");
|
||||
prompt.append("3. 短期改善计划(1-4周)\n");
|
||||
prompt.append("4. 长期健康目标建议\n\n");
|
||||
|
||||
prompt.append("请以JSON格式返回,包含以下结构:\n");
|
||||
prompt.append("{\n");
|
||||
prompt.append(" \"lifestyle\": {\n");
|
||||
prompt.append(" \"diet\": [\"饮食建议1\", \"饮食建议2\"],\n");
|
||||
prompt.append(" \"exercise\": [\"运动建议1\", \"运动建议2\"],\n");
|
||||
prompt.append(" \"sleep\": [\"睡眠建议1\", \"睡眠建议2\"]\n");
|
||||
prompt.append(" },\n");
|
||||
prompt.append(" \"riskPrevention\": [\"预防措施1\", \"预防措施2\"],\n");
|
||||
prompt.append(" \"shortTermPlan\": {\n");
|
||||
prompt.append(" \"duration\": \"2周\",\n");
|
||||
prompt.append(" \"goals\": [\"目标1\", \"目标2\"],\n");
|
||||
prompt.append(" \"actions\": [\"行动1\", \"行动2\"]\n");
|
||||
prompt.append(" },\n");
|
||||
prompt.append(" \"longTermGoals\": [\"长期目标1\", \"长期目标2\"],\n");
|
||||
prompt.append(" \"priority\": \"high|medium|low\"\n");
|
||||
prompt.append("}\n");
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Ollama模型
|
||||
*/
|
||||
private String callOllamaModel(String prompt) {
|
||||
try {
|
||||
String url = ollamaApiUrl + "/api/generate";
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", modelName);
|
||||
requestBody.put("prompt", prompt);
|
||||
requestBody.put("stream", false);
|
||||
requestBody.put("options", Map.of(
|
||||
"temperature", 0.3,
|
||||
"top_p", 0.9,
|
||||
"max_tokens", 2000
|
||||
));
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
log.debug("🚀 调用Ollama模型: {} -> {}", modelName, url);
|
||||
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url, HttpMethod.POST, entity, String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
return response.getBody();
|
||||
} else {
|
||||
throw new BizException("Ollama模型调用失败: HTTP " + response.getStatusCode());
|
||||
}
|
||||
|
||||
} catch (ResourceAccessException e) {
|
||||
log.error("🚫 Ollama服务连接失败: {}", e.getMessage());
|
||||
throw new BizException("无法连接到Ollama服务,请检查服务状态");
|
||||
} catch (Exception e) {
|
||||
log.error("🚫 Ollama模型调用异常: {}", e.getMessage(), e);
|
||||
throw new BizException("模型调用失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析健康预测结果
|
||||
*/
|
||||
private HealthPredictionResult parseHealthPrediction(String response, Long userId, UserHealthContext context) {
|
||||
try {
|
||||
// 尝试解析JSON响应
|
||||
JsonNode jsonResponse = objectMapper.readTree(response);
|
||||
JsonNode predictionNode = jsonResponse.has("response") ?
|
||||
objectMapper.readTree(jsonResponse.get("response").asText()) : jsonResponse;
|
||||
|
||||
HealthPredictionResult result = new HealthPredictionResult();
|
||||
result.setUserId(userId);
|
||||
result.setGeneratedAt(LocalDateTime.now());
|
||||
result.setModelVersion(modelName);
|
||||
|
||||
// 解析预测内容
|
||||
if (predictionNode.has("healthTrend")) {
|
||||
result.setHealthTrend(predictionNode.get("healthTrend").asText());
|
||||
}
|
||||
|
||||
if (predictionNode.has("riskFactors")) {
|
||||
List<String> risks = new ArrayList<>();
|
||||
predictionNode.get("riskFactors").forEach(node -> risks.add(node.asText()));
|
||||
result.setRiskFactors(risks);
|
||||
}
|
||||
|
||||
if (predictionNode.has("keyIndicators")) {
|
||||
List<String> indicators = new ArrayList<>();
|
||||
predictionNode.get("keyIndicators").forEach(node -> indicators.add(node.asText()));
|
||||
result.setKeyIndicators(indicators);
|
||||
}
|
||||
|
||||
if (predictionNode.has("confidence")) {
|
||||
result.setConfidence(predictionNode.get("confidence").asDouble());
|
||||
}
|
||||
|
||||
if (predictionNode.has("recommendations")) {
|
||||
List<String> recommendations = new ArrayList<>();
|
||||
predictionNode.get("recommendations").forEach(node -> recommendations.add(node.asText()));
|
||||
result.setRecommendations(recommendations);
|
||||
}
|
||||
|
||||
// 设置原始响应
|
||||
result.setRawResponse(response);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ 解析模型响应失败,使用原始文本: {}", e.getMessage());
|
||||
|
||||
// 如果JSON解析失败,创建基础结果
|
||||
HealthPredictionResult result = new HealthPredictionResult();
|
||||
result.setUserId(userId);
|
||||
result.setGeneratedAt(LocalDateTime.now());
|
||||
result.setModelVersion(modelName);
|
||||
result.setHealthTrend("基于AI模型分析的健康预测");
|
||||
result.setRawResponse(response);
|
||||
result.setConfidence(0.7);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析健康建议结果
|
||||
*/
|
||||
private HealthAdviceResult parseHealthAdvice(String response, Long userId, UserHealthContext context) {
|
||||
try {
|
||||
JsonNode jsonResponse = objectMapper.readTree(response);
|
||||
JsonNode adviceNode = jsonResponse.has("response") ?
|
||||
objectMapper.readTree(jsonResponse.get("response").asText()) : jsonResponse;
|
||||
|
||||
HealthAdviceResult result = new HealthAdviceResult();
|
||||
result.setUserId(userId);
|
||||
result.setGeneratedAt(LocalDateTime.now());
|
||||
result.setModelVersion(modelName);
|
||||
|
||||
// 解析生活方式建议
|
||||
if (adviceNode.has("lifestyle")) {
|
||||
JsonNode lifestyle = adviceNode.get("lifestyle");
|
||||
Map<String, List<String>> lifestyleAdvice = new HashMap<>();
|
||||
|
||||
if (lifestyle.has("diet")) {
|
||||
List<String> diet = new ArrayList<>();
|
||||
lifestyle.get("diet").forEach(node -> diet.add(node.asText()));
|
||||
lifestyleAdvice.put("diet", diet);
|
||||
}
|
||||
|
||||
if (lifestyle.has("exercise")) {
|
||||
List<String> exercise = new ArrayList<>();
|
||||
lifestyle.get("exercise").forEach(node -> exercise.add(node.asText()));
|
||||
lifestyleAdvice.put("exercise", exercise);
|
||||
}
|
||||
|
||||
if (lifestyle.has("sleep")) {
|
||||
List<String> sleep = new ArrayList<>();
|
||||
lifestyle.get("sleep").forEach(node -> sleep.add(node.asText()));
|
||||
lifestyleAdvice.put("sleep", sleep);
|
||||
}
|
||||
|
||||
result.setLifestyleAdvice(lifestyleAdvice);
|
||||
}
|
||||
|
||||
// 解析风险预防
|
||||
if (adviceNode.has("riskPrevention")) {
|
||||
List<String> prevention = new ArrayList<>();
|
||||
adviceNode.get("riskPrevention").forEach(node -> prevention.add(node.asText()));
|
||||
result.setRiskPrevention(prevention);
|
||||
}
|
||||
|
||||
// 解析短期计划
|
||||
if (adviceNode.has("shortTermPlan")) {
|
||||
JsonNode plan = adviceNode.get("shortTermPlan");
|
||||
Map<String, Object> shortTermPlan = new HashMap<>();
|
||||
|
||||
if (plan.has("duration")) {
|
||||
shortTermPlan.put("duration", plan.get("duration").asText());
|
||||
}
|
||||
|
||||
if (plan.has("goals")) {
|
||||
List<String> goals = new ArrayList<>();
|
||||
plan.get("goals").forEach(node -> goals.add(node.asText()));
|
||||
shortTermPlan.put("goals", goals);
|
||||
}
|
||||
|
||||
if (plan.has("actions")) {
|
||||
List<String> actions = new ArrayList<>();
|
||||
plan.get("actions").forEach(node -> actions.add(node.asText()));
|
||||
shortTermPlan.put("actions", actions);
|
||||
}
|
||||
|
||||
result.setShortTermPlan(shortTermPlan);
|
||||
}
|
||||
|
||||
// 解析长期目标
|
||||
if (adviceNode.has("longTermGoals")) {
|
||||
List<String> longTermGoals = new ArrayList<>();
|
||||
adviceNode.get("longTermGoals").forEach(node -> longTermGoals.add(node.asText()));
|
||||
result.setLongTermGoals(longTermGoals);
|
||||
}
|
||||
|
||||
// 解析优先级
|
||||
if (adviceNode.has("priority")) {
|
||||
result.setPriority(adviceNode.get("priority").asText());
|
||||
}
|
||||
|
||||
result.setRawResponse(response);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ 解析建议响应失败,使用原始文本: {}", e.getMessage());
|
||||
|
||||
HealthAdviceResult result = new HealthAdviceResult();
|
||||
result.setUserId(userId);
|
||||
result.setGeneratedAt(LocalDateTime.now());
|
||||
result.setModelVersion(modelName);
|
||||
result.setRawResponse(response);
|
||||
result.setPriority("medium");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算健康数据统计信息
|
||||
*/
|
||||
private Map<String, String> calculateHealthStatistics(List<UserHealthData> healthDataList) {
|
||||
Map<String, String> statistics = new HashMap<>();
|
||||
|
||||
if (healthDataList == null || healthDataList.isEmpty()) {
|
||||
return statistics;
|
||||
}
|
||||
|
||||
// 心率统计
|
||||
List<Integer> heartRates = healthDataList.stream()
|
||||
.filter(data -> data.getHeartRate() != null && data.getHeartRate() > 0)
|
||||
.map(UserHealthData::getHeartRate)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!heartRates.isEmpty()) {
|
||||
double avgHeartRate = heartRates.stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
statistics.put("heart_rate", String.format("平均 %.1f bpm (范围: %d-%d)",
|
||||
avgHeartRate, Collections.min(heartRates), Collections.max(heartRates)));
|
||||
}
|
||||
|
||||
// 血氧统计
|
||||
List<Integer> bloodOxygens = healthDataList.stream()
|
||||
.filter(data -> data.getBloodOxygen() != null && data.getBloodOxygen() > 0)
|
||||
.map(UserHealthData::getBloodOxygen)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!bloodOxygens.isEmpty()) {
|
||||
double avgBloodOxygen = bloodOxygens.stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
statistics.put("blood_oxygen", String.format("平均 %.1f%% (范围: %d%%-%d%%)",
|
||||
avgBloodOxygen, Collections.min(bloodOxygens), Collections.max(bloodOxygens)));
|
||||
}
|
||||
|
||||
// 步数统计
|
||||
List<Integer> steps = healthDataList.stream()
|
||||
.filter(data -> data.getStep() != null && data.getStep() > 0)
|
||||
.map(UserHealthData::getStep)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!steps.isEmpty()) {
|
||||
double avgSteps = steps.stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
statistics.put("step", String.format("日均 %.0f 步", avgSteps));
|
||||
}
|
||||
|
||||
// 睡眠统计
|
||||
List<Integer> sleeps = healthDataList.stream()
|
||||
.filter(data -> data.getSleep() != null && data.getSleep() > 0)
|
||||
.map(UserHealthData::getSleep)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!sleeps.isEmpty()) {
|
||||
double avgSleep = sleeps.stream().mapToInt(Integer::intValue).average().orElse(0.0);
|
||||
statistics.put("sleep", String.format("平均 %.1f 小时", avgSleep / 60.0));
|
||||
}
|
||||
|
||||
return statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特征显示名称
|
||||
*/
|
||||
private String getFeatureDisplayName(String featureName) {
|
||||
Map<String, String> featureNames = Map.of(
|
||||
"heart_rate", "心率",
|
||||
"blood_oxygen", "血氧",
|
||||
"temperature", "体温",
|
||||
"pressure_high", "收缩压",
|
||||
"pressure_low", "舒张压",
|
||||
"stress", "压力指数",
|
||||
"step", "步数",
|
||||
"sleep", "睡眠",
|
||||
"calorie", "卡路里",
|
||||
"distance", "距离"
|
||||
);
|
||||
|
||||
return featureNames.getOrDefault(featureName, featureName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Ollama服务状态
|
||||
*/
|
||||
public boolean checkOllamaHealth() {
|
||||
try {
|
||||
String url = ollamaApiUrl + "/api/tags";
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
return response.getStatusCode().is2xxSuccessful();
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ Ollama健康检查失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用模型列表
|
||||
*/
|
||||
public List<String> getAvailableModels() {
|
||||
try {
|
||||
String url = ollamaApiUrl + "/api/tags";
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JsonNode jsonResponse = objectMapper.readTree(response.getBody());
|
||||
List<String> models = new ArrayList<>();
|
||||
|
||||
if (jsonResponse.has("models")) {
|
||||
jsonResponse.get("models").forEach(model -> {
|
||||
if (model.has("name")) {
|
||||
models.add(model.get("name").asText());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 获取模型列表失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// === 内部数据类 ===
|
||||
|
||||
/**
|
||||
* 用户健康数据上下文
|
||||
*/
|
||||
public static class UserHealthContext {
|
||||
private Long userId;
|
||||
private List<UserHealthData> recentHealthData;
|
||||
private List<HealthBaseline> baselines;
|
||||
private Map<String, String> statistics;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public List<UserHealthData> getRecentHealthData() { return recentHealthData; }
|
||||
public void setRecentHealthData(List<UserHealthData> recentHealthData) { this.recentHealthData = recentHealthData; }
|
||||
|
||||
public List<HealthBaseline> getBaselines() { return baselines; }
|
||||
public void setBaselines(List<HealthBaseline> baselines) { this.baselines = baselines; }
|
||||
|
||||
public Map<String, String> getStatistics() { return statistics; }
|
||||
public void setStatistics(Map<String, String> statistics) { this.statistics = statistics; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康预测结果
|
||||
*/
|
||||
public static class HealthPredictionResult {
|
||||
private Long userId;
|
||||
private LocalDateTime generatedAt;
|
||||
private String modelVersion;
|
||||
private String healthTrend;
|
||||
private List<String> riskFactors;
|
||||
private List<String> keyIndicators;
|
||||
private Double confidence;
|
||||
private List<String> recommendations;
|
||||
private String rawResponse;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getGeneratedAt() { return generatedAt; }
|
||||
public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }
|
||||
|
||||
public String getModelVersion() { return modelVersion; }
|
||||
public void setModelVersion(String modelVersion) { this.modelVersion = modelVersion; }
|
||||
|
||||
public String getHealthTrend() { return healthTrend; }
|
||||
public void setHealthTrend(String healthTrend) { this.healthTrend = healthTrend; }
|
||||
|
||||
public List<String> getRiskFactors() { return riskFactors; }
|
||||
public void setRiskFactors(List<String> riskFactors) { this.riskFactors = riskFactors; }
|
||||
|
||||
public List<String> getKeyIndicators() { return keyIndicators; }
|
||||
public void setKeyIndicators(List<String> keyIndicators) { this.keyIndicators = keyIndicators; }
|
||||
|
||||
public Double getConfidence() { return confidence; }
|
||||
public void setConfidence(Double confidence) { this.confidence = confidence; }
|
||||
|
||||
public List<String> getRecommendations() { return recommendations; }
|
||||
public void setRecommendations(List<String> recommendations) { this.recommendations = recommendations; }
|
||||
|
||||
public String getRawResponse() { return rawResponse; }
|
||||
public void setRawResponse(String rawResponse) { this.rawResponse = rawResponse; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康建议结果
|
||||
*/
|
||||
public static class HealthAdviceResult {
|
||||
private Long userId;
|
||||
private LocalDateTime generatedAt;
|
||||
private String modelVersion;
|
||||
private Map<String, List<String>> lifestyleAdvice;
|
||||
private List<String> riskPrevention;
|
||||
private Map<String, Object> shortTermPlan;
|
||||
private List<String> longTermGoals;
|
||||
private String priority;
|
||||
private String rawResponse;
|
||||
|
||||
// Getters and Setters
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public LocalDateTime getGeneratedAt() { return generatedAt; }
|
||||
public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }
|
||||
|
||||
public String getModelVersion() { return modelVersion; }
|
||||
public void setModelVersion(String modelVersion) { this.modelVersion = modelVersion; }
|
||||
|
||||
public Map<String, List<String>> getLifestyleAdvice() { return lifestyleAdvice; }
|
||||
public void setLifestyleAdvice(Map<String, List<String>> lifestyleAdvice) { this.lifestyleAdvice = lifestyleAdvice; }
|
||||
|
||||
public List<String> getRiskPrevention() { return riskPrevention; }
|
||||
public void setRiskPrevention(List<String> riskPrevention) { this.riskPrevention = riskPrevention; }
|
||||
|
||||
public Map<String, Object> getShortTermPlan() { return shortTermPlan; }
|
||||
public void setShortTermPlan(Map<String, Object> shortTermPlan) { this.shortTermPlan = shortTermPlan; }
|
||||
|
||||
public List<String> getLongTermGoals() { return longTermGoals; }
|
||||
public void setLongTermGoals(List<String> longTermGoals) { this.longTermGoals = longTermGoals; }
|
||||
|
||||
public String getPriority() { return priority; }
|
||||
public void setPriority(String priority) { this.priority = priority; }
|
||||
|
||||
public String getRawResponse() { return rawResponse; }
|
||||
public void setRawResponse(String rawResponse) { this.rawResponse = rawResponse; }
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
||||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
||||
gradle.user.home=
|
||||
java.home=/Library/Java/JavaVirtualMachines/amazon-corretto-8.jdk/Contents/Home
|
||||
java.home=/opt/homebrew/Cellar/openjdk@21/21.0.8/libexec/openjdk.jdk/Contents/Home
|
||||
jvm.arguments=
|
||||
offline.mode=false
|
||||
override.workspace.settings=true
|
||||
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
# AI集成测试脚本
|
||||
echo "🔍 开始AI集成测试..."
|
||||
|
||||
# 检查Ollama服务状态
|
||||
echo "📡 检查Ollama服务状态..."
|
||||
OLLAMA_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://192.168.1.83:11434/api/tags)
|
||||
if [ "$OLLAMA_STATUS" = "200" ]; then
|
||||
echo "✅ Ollama服务运行正常 (HTTP $OLLAMA_STATUS)"
|
||||
else
|
||||
echo "❌ Ollama服务连接失败 (HTTP $OLLAMA_STATUS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查健康模型是否存在
|
||||
echo "🧠 检查ljwx-health-enhanced模型..."
|
||||
MODEL_EXISTS=$(curl -s http://192.168.1.83:11434/api/tags | grep -c "ljwx-health-enhanced:latest")
|
||||
if [ "$MODEL_EXISTS" -gt 0 ]; then
|
||||
echo "✅ ljwx-health-enhanced:latest 模型已加载"
|
||||
else
|
||||
echo "❌ ljwx-health-enhanced:latest 模型未找到"
|
||||
echo "📋 可用模型列表:"
|
||||
curl -s http://192.168.1.83:11434/api/tags | python3 -c "import sys, json; models = json.load(sys.stdin)['models']; [print(f' - {m[\"name\"]}') for m in models]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 测试模型响应
|
||||
echo "🧪 测试模型响应..."
|
||||
RESP=$(curl -s -X POST http://192.168.1.83:11434/api/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "ljwx-health-enhanced:latest",
|
||||
"prompt": "简单回答:你好",
|
||||
"stream": false
|
||||
}')
|
||||
|
||||
if echo "$RESP" | grep -q '"done":true'; then
|
||||
echo "✅ 模型响应正常"
|
||||
RESPONSE_TEXT=$(echo "$RESP" | python3 -c "import sys, json; print(json.load(sys.stdin)['response'][:100] + '...')")
|
||||
echo "📝 响应内容: $RESPONSE_TEXT"
|
||||
else
|
||||
echo "❌ 模型响应异常"
|
||||
echo "🔍 错误详情: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查ljwx-boot是否运行
|
||||
echo "🚀 检查ljwx-boot服务..."
|
||||
if pgrep -f "ljwx-boot" > /dev/null; then
|
||||
echo "✅ ljwx-boot服务正在运行"
|
||||
BOOT_PID=$(pgrep -f "ljwx-boot")
|
||||
echo "📋 进程ID: $BOOT_PID"
|
||||
else
|
||||
echo "⚠️ ljwx-boot服务未运行"
|
||||
echo "💡 请使用以下命令启动服务:"
|
||||
echo " cd ljwx-boot && ./run-local.sh start"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 AI集成测试完成!"
|
||||
echo ""
|
||||
echo "📖 使用说明:"
|
||||
echo "1. 确保ljwx-boot服务运行: cd ljwx-boot && ./run-local.sh start"
|
||||
echo "2. 启动前端服务: cd ljwx-admin && npm run dev"
|
||||
echo "3. 访问健康预测页面并点击'AI预测'按钮"
|
||||
echo "4. 测试AI健康预测和建议功能"
|
||||
echo ""
|
||||
echo "🔗 相关链接:"
|
||||
echo "- Ollama服务: http://192.168.1.83:11434"
|
||||
echo "- 后端API: http://localhost:9998/t_health_prediction/ai/health"
|
||||
echo "- 前端页面: http://localhost:3000/#/health/prediction"
|
||||
Reference in New Issue
Block a user