diff --git a/AI_INTEGRATION_GUIDE.md b/AI_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..9e4759a --- /dev/null +++ b/AI_INTEGRATION_GUIDE.md @@ -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 healthIssues); + public boolean checkOllamaHealth(); + public List 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能力,提升了用户体验和健康管理的智能化水平。 \ No newline at end of file diff --git a/ljwx-admin/src/router/elegant/imports.ts b/ljwx-admin/src/router/elegant/imports.ts index 5c330ca..c7fc5dd 100644 --- a/ljwx-admin/src/router/elegant/imports.ts +++ b/ljwx-admin/src/router/elegant/imports.ts @@ -48,6 +48,7 @@ export const views: Record Promise 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"), diff --git a/ljwx-admin/src/router/elegant/routes.ts b/ljwx-admin/src/router/elegant/routes.ts index 9f0a496..bb51c18 100644 --- a/ljwx-admin/src/router/elegant/routes.ts +++ b/ljwx-admin/src/router/elegant/routes.ts @@ -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', diff --git a/ljwx-admin/src/router/elegant/transform.ts b/ljwx-admin/src/router/elegant/transform.ts index 18b547d..641a0b3 100644 --- a/ljwx-admin/src/router/elegant/transform.ts +++ b/ljwx-admin/src/router/elegant/transform.ts @@ -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", diff --git a/ljwx-admin/src/service/api/health/ai-prediction.ts b/ljwx-admin/src/service/api/health/ai-prediction.ts new file mode 100644 index 0000000..d11a4c3 --- /dev/null +++ b/ljwx-admin/src/service/api/health/ai-prediction.ts @@ -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; +} + +/** + * AI健康预测服务 + */ +export const aiPredictionApi = { + /** + * AI健康预测 + */ + predict: (userId: number, days: number = 7) => { + return request.post('/t_health_prediction/ai/predict', null, { + params: { userId, days } + }); + }, + + /** + * AI健康建议 + */ + advice: (userId: number, healthIssues?: string[]) => { + return request.post('/t_health_prediction/ai/advice', null, { + params: { userId, healthIssues } + }); + }, + + /** + * 获取可用AI模型列表 + */ + getAvailableModels: () => { + return request.get('/t_health_prediction/ai/models'); + }, + + /** + * 检查AI服务健康状态 + */ + checkHealth: () => { + return request.get('/t_health_prediction/ai/health'); + }, + + /** + * 批量AI健康预测 + */ + batchPredict: (userIds: number[], days: number = 7) => { + return request.post('/t_health_prediction/ai/batch-predict', userIds, { + params: { days } + }); + } +}; diff --git a/ljwx-admin/src/service/api/health/index.ts b/ljwx-admin/src/service/api/health/index.ts index f6a4bc2..c0c8891 100644 --- a/ljwx-admin/src/service/api/health/index.ts +++ b/ljwx-admin/src/service/api/health/index.ts @@ -15,3 +15,4 @@ export * from './health-baseline'; export * from './health-score'; export * from './health-feature'; export * from './device-binding'; +export * from './ai-prediction'; diff --git a/ljwx-admin/src/typings/components.d.ts b/ljwx-admin/src/typings/components.d.ts index 332af20..fb9fabc 100644 --- a/ljwx-admin/src/typings/components.d.ts +++ b/ljwx-admin/src/typings/components.d.ts @@ -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'] diff --git a/ljwx-admin/src/typings/elegant-router.d.ts b/ljwx-admin/src/typings/elegant-router.d.ts index 25d0618..afb434f 100644 --- a/ljwx-admin/src/typings/elegant-router.d.ts +++ b/ljwx-admin/src/typings/elegant-router.d.ts @@ -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" diff --git a/ljwx-admin/src/views/health/ai-models/index.vue b/ljwx-admin/src/views/health/ai-models/index.vue new file mode 100644 index 0000000..27018a5 --- /dev/null +++ b/ljwx-admin/src/views/health/ai-models/index.vue @@ -0,0 +1,387 @@ + + + + + diff --git a/ljwx-admin/src/views/health/prediction/index.vue b/ljwx-admin/src/views/health/prediction/index.vue index 46d4cf7..5d63945 100644 --- a/ljwx-admin/src/views/health/prediction/index.vue +++ b/ljwx-admin/src/views/health/prediction/index.vue @@ -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 = 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(() => { 模型管理 + + + AI预测 + @@ -458,6 +470,11 @@ const taskStats = computed(() => { + + + + + diff --git a/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/THealthPredictionController.java b/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/THealthPredictionController.java index b9c6aca..d351a21 100644 --- a/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/THealthPredictionController.java +++ b/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/health/THealthPredictionController.java @@ -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 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 aiAdvice( + @Parameter(description = "用户ID") @RequestParam("userId") Long userId, + @Parameter(description = "健康问题") @RequestParam(value = "healthIssues", required = false) List 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> getAvailableModels() { + log.info("📋 获取可用AI模型列表"); + + try { + List 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> checkAiHealth() { + log.info("🏥 检查AI服务健康状态"); + + try { + boolean isHealthy = ollamaHealthPredictionService.checkOllamaHealth(); + List models = ollamaHealthPredictionService.getAvailableModels(); + + Map 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> batchAiPredict( + @Parameter(description = "用户ID列表") @RequestBody List userIds, + @Parameter(description = "预测天数") @RequestParam(value = "days", defaultValue = "7") Integer days) { + + log.info("🔮 批量AI健康预测 - userIds: {}, days: {}", userIds, days); + + try { + Map 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()); + } + } } \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/test/AiTestController.java b/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/test/AiTestController.java new file mode 100644 index 0000000..2e713a0 --- /dev/null +++ b/ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/test/AiTestController.java @@ -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 ping() { + log.info("🏓 AI测试 - ping请求"); + return Result.data("pong - AI测试接口正常"); + } + + @GetMapping("/health") + @Operation(summary = "AI服务健康检查(无权限)") + public Result> checkHealth() { + log.info("🔍 AI测试 - 健康检查"); + + try { + boolean healthy = ollamaHealthPredictionService.checkOllamaHealth(); + List models = ollamaHealthPredictionService.getAvailableModels(); + + Map 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 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 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 mockAdvice( + @Parameter(description = "用户ID") @RequestParam("userId") Long userId, + @Parameter(description = "健康问题") @RequestParam(value = "healthIssues", required = false) List 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> 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 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 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()); + } + } +} \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-admin/src/main/resources/application.yml b/ljwx-boot/ljwx-boot-admin/src/main/resources/application.yml index cf73efe..26e7d32 100644 --- a/ljwx-boot/ljwx-boot-admin/src/main/resources/application.yml +++ b/ljwx-boot/ljwx-boot-admin/src/main/resources/application.yml @@ -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} \ No newline at end of file + 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} \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-infrastructure/src/main/java/com/ljwx/infrastructure/config/InterceptorConfiguration.java b/ljwx-boot/ljwx-boot-infrastructure/src/main/java/com/ljwx/infrastructure/config/InterceptorConfiguration.java index 0302e83..069f943 100644 --- a/ljwx-boot/ljwx-boot-infrastructure/src/main/java/com/ljwx/infrastructure/config/InterceptorConfiguration.java +++ b/ljwx-boot/ljwx-boot-infrastructure/src/main/java/com/ljwx/infrastructure/config/InterceptorConfiguration.java @@ -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); } } diff --git a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/config/OllamaConfig.java b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/config/OllamaConfig.java new file mode 100644 index 0000000..db86355 --- /dev/null +++ b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/config/OllamaConfig.java @@ -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; + } +} \ No newline at end of file diff --git a/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/OllamaHealthPredictionService.java b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/OllamaHealthPredictionService.java new file mode 100644 index 0000000..67a929a --- /dev/null +++ b/ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/OllamaHealthPredictionService.java @@ -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 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 dataQuery = new QueryWrapper<>(); + dataQuery.eq("user_id", userId) + .ge("timestamp", startTime) + .le("timestamp", endTime) + .eq("is_deleted", 0) + .orderByDesc("timestamp") + .last("LIMIT 1000"); // 限制数据量 + + List healthDataList = userHealthDataMapper.selectList(dataQuery); + context.setRecentHealthData(healthDataList); + + // 获取健康基线 + QueryWrapper baselineQuery = new QueryWrapper<>(); + baselineQuery.eq("user_id", userId) + .eq("is_current", 1) + .eq("is_deleted", 0); + + List 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 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 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> entity = new HttpEntity<>(requestBody, headers); + + log.debug("🚀 调用Ollama模型: {} -> {}", modelName, url); + + // 发送请求 + ResponseEntity 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 risks = new ArrayList<>(); + predictionNode.get("riskFactors").forEach(node -> risks.add(node.asText())); + result.setRiskFactors(risks); + } + + if (predictionNode.has("keyIndicators")) { + List 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 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> lifestyleAdvice = new HashMap<>(); + + if (lifestyle.has("diet")) { + List diet = new ArrayList<>(); + lifestyle.get("diet").forEach(node -> diet.add(node.asText())); + lifestyleAdvice.put("diet", diet); + } + + if (lifestyle.has("exercise")) { + List exercise = new ArrayList<>(); + lifestyle.get("exercise").forEach(node -> exercise.add(node.asText())); + lifestyleAdvice.put("exercise", exercise); + } + + if (lifestyle.has("sleep")) { + List sleep = new ArrayList<>(); + lifestyle.get("sleep").forEach(node -> sleep.add(node.asText())); + lifestyleAdvice.put("sleep", sleep); + } + + result.setLifestyleAdvice(lifestyleAdvice); + } + + // 解析风险预防 + if (adviceNode.has("riskPrevention")) { + List 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 shortTermPlan = new HashMap<>(); + + if (plan.has("duration")) { + shortTermPlan.put("duration", plan.get("duration").asText()); + } + + if (plan.has("goals")) { + List goals = new ArrayList<>(); + plan.get("goals").forEach(node -> goals.add(node.asText())); + shortTermPlan.put("goals", goals); + } + + if (plan.has("actions")) { + List actions = new ArrayList<>(); + plan.get("actions").forEach(node -> actions.add(node.asText())); + shortTermPlan.put("actions", actions); + } + + result.setShortTermPlan(shortTermPlan); + } + + // 解析长期目标 + if (adviceNode.has("longTermGoals")) { + List 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 calculateHealthStatistics(List healthDataList) { + Map statistics = new HashMap<>(); + + if (healthDataList == null || healthDataList.isEmpty()) { + return statistics; + } + + // 心率统计 + List 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 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 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 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 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 response = restTemplate.getForEntity(url, String.class); + return response.getStatusCode().is2xxSuccessful(); + } catch (Exception e) { + log.warn("⚠️ Ollama健康检查失败: {}", e.getMessage()); + return false; + } + } + + /** + * 获取可用模型列表 + */ + public List getAvailableModels() { + try { + String url = ollamaApiUrl + "/api/tags"; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + JsonNode jsonResponse = objectMapper.readTree(response.getBody()); + List 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 recentHealthData; + private List baselines; + private Map statistics; + + // Getters and Setters + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + + public List getRecentHealthData() { return recentHealthData; } + public void setRecentHealthData(List recentHealthData) { this.recentHealthData = recentHealthData; } + + public List getBaselines() { return baselines; } + public void setBaselines(List baselines) { this.baselines = baselines; } + + public Map getStatistics() { return statistics; } + public void setStatistics(Map statistics) { this.statistics = statistics; } + } + + /** + * 健康预测结果 + */ + public static class HealthPredictionResult { + private Long userId; + private LocalDateTime generatedAt; + private String modelVersion; + private String healthTrend; + private List riskFactors; + private List keyIndicators; + private Double confidence; + private List 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 getRiskFactors() { return riskFactors; } + public void setRiskFactors(List riskFactors) { this.riskFactors = riskFactors; } + + public List getKeyIndicators() { return keyIndicators; } + public void setKeyIndicators(List keyIndicators) { this.keyIndicators = keyIndicators; } + + public Double getConfidence() { return confidence; } + public void setConfidence(Double confidence) { this.confidence = confidence; } + + public List getRecommendations() { return recommendations; } + public void setRecommendations(List 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> lifestyleAdvice; + private List riskPrevention; + private Map shortTermPlan; + private List 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> getLifestyleAdvice() { return lifestyleAdvice; } + public void setLifestyleAdvice(Map> lifestyleAdvice) { this.lifestyleAdvice = lifestyleAdvice; } + + public List getRiskPrevention() { return riskPrevention; } + public void setRiskPrevention(List riskPrevention) { this.riskPrevention = riskPrevention; } + + public Map getShortTermPlan() { return shortTermPlan; } + public void setShortTermPlan(Map shortTermPlan) { this.shortTermPlan = shortTermPlan; } + + public List getLongTermGoals() { return longTermGoals; } + public void setLongTermGoals(List 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; } + } +} \ No newline at end of file diff --git a/ljwx-watch/.gradle/7.3/checksums/checksums.lock b/ljwx-watch/.gradle/7.3/checksums/checksums.lock index 43a890d..fba5b53 100644 Binary files a/ljwx-watch/.gradle/7.3/checksums/checksums.lock and b/ljwx-watch/.gradle/7.3/checksums/checksums.lock differ diff --git a/ljwx-watch/.gradle/7.3/checksums/md5-checksums.bin b/ljwx-watch/.gradle/7.3/checksums/md5-checksums.bin index 17a49a4..c4c62bc 100644 Binary files a/ljwx-watch/.gradle/7.3/checksums/md5-checksums.bin and b/ljwx-watch/.gradle/7.3/checksums/md5-checksums.bin differ diff --git a/ljwx-watch/.gradle/7.3/checksums/sha1-checksums.bin b/ljwx-watch/.gradle/7.3/checksums/sha1-checksums.bin index ed00d0a..079b632 100644 Binary files a/ljwx-watch/.gradle/7.3/checksums/sha1-checksums.bin and b/ljwx-watch/.gradle/7.3/checksums/sha1-checksums.bin differ diff --git a/ljwx-watch/.settings/org.eclipse.buildship.core.prefs b/ljwx-watch/.settings/org.eclipse.buildship.core.prefs index 2999fbf..3e4d5eb 100644 --- a/ljwx-watch/.settings/org.eclipse.buildship.core.prefs +++ b/ljwx-watch/.settings/org.eclipse.buildship.core.prefs @@ -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 diff --git a/test_ai_integration.sh b/test_ai_integration.sh new file mode 100755 index 0000000..7065045 --- /dev/null +++ b/test_ai_integration.sh @@ -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"