fully commits

This commit is contained in:
2025-11-19 20:08:13 +08:00
parent 99aa240439
commit eeb99652c5
21 changed files with 1944 additions and 2 deletions
+320
View File
@@ -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能力,提升了用户体验和健康管理的智能化水平。
+1
View File
@@ -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"),
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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>
@@ -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());
}
}
}
@@ -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}
@@ -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);
}
}
@@ -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;
}
}
@@ -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
+72
View File
@@ -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"