From eeb99652c55efd5189336dbf0d8a7320f5e3ba65 Mon Sep 17 00:00:00 2001 From: gao Date: Wed, 19 Nov 2025 20:08:13 +0800 Subject: [PATCH] fully commits --- AI_INTEGRATION_GUIDE.md | 320 ++++++++ ljwx-admin/src/router/elegant/imports.ts | 1 + ljwx-admin/src/router/elegant/routes.ts | 9 + ljwx-admin/src/router/elegant/transform.ts | 1 + .../src/service/api/health/ai-prediction.ts | 93 +++ ljwx-admin/src/service/api/health/index.ts | 1 + ljwx-admin/src/typings/components.d.ts | 3 + ljwx-admin/src/typings/elegant-router.d.ts | 2 + .../src/views/health/ai-models/index.vue | 387 ++++++++++ .../src/views/health/prediction/index.vue | 17 + .../health/THealthPredictionController.java | 112 +++ .../controller/test/AiTestController.java | 160 ++++ .../src/main/resources/application.yml | 15 +- .../config/InterceptorConfiguration.java | 7 + .../modules/health/config/OllamaConfig.java | 36 + .../OllamaHealthPredictionService.java | 708 ++++++++++++++++++ .../.gradle/7.3/checksums/checksums.lock | Bin 17 -> 17 bytes .../.gradle/7.3/checksums/md5-checksums.bin | Bin 20297 -> 24397 bytes .../.gradle/7.3/checksums/sha1-checksums.bin | Bin 24275 -> 35615 bytes .../org.eclipse.buildship.core.prefs | 2 +- test_ai_integration.sh | 72 ++ 21 files changed, 1944 insertions(+), 2 deletions(-) create mode 100644 AI_INTEGRATION_GUIDE.md create mode 100644 ljwx-admin/src/service/api/health/ai-prediction.ts create mode 100644 ljwx-admin/src/views/health/ai-models/index.vue create mode 100644 ljwx-boot/ljwx-boot-admin/src/main/java/com/ljwx/admin/controller/test/AiTestController.java create mode 100644 ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/config/OllamaConfig.java create mode 100644 ljwx-boot/ljwx-boot-modules/src/main/java/com/ljwx/modules/health/service/OllamaHealthPredictionService.java create mode 100755 test_ai_integration.sh 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 43a890d23561f6c17767ac37ef6bc69c067f4a3a..fba5b5397f130b19b4e69d7dc1f5f98d452ef842 100644 GIT binary patch literal 17 VcmZQxdc}mlC+3h70~jzK0{}101aklY literal 17 UcmZQxdc}mlC+3h70|ZP5056;bLjV8( diff --git a/ljwx-watch/.gradle/7.3/checksums/md5-checksums.bin b/ljwx-watch/.gradle/7.3/checksums/md5-checksums.bin index 17a49a4341fccd02b9ec4a9eec3773e3ee5ecab9..c4c62bcd0aadf25617cb5d3b841b34d246fe7966 100644 GIT binary patch literal 24397 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 delta 145 zcmX@RkMZO@#tkMCf+|fP|1Q}nroqAh20kAr_exkyo+~La`7aQ=N_k9PDrGR4Ra#>5 zQ6T1(k(ewhD>1ngh?mL&#pOIErvmX&If==(@{*JDn+a diff --git a/ljwx-watch/.gradle/7.3/checksums/sha1-checksums.bin b/ljwx-watch/.gradle/7.3/checksums/sha1-checksums.bin index ed00d0a72a9cb616703d3ca85245f50580bf4a71..079b632e438b055af40526f798a45268ef12eaa3 100644 GIT binary patch literal 35615 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 KNfEtJ)&BtI+a)>x delta 337 zcmXZXElk5e6bJA^06`dnq`P;8uH8r1eYL%^RZd{GBIXzx1Q-HtI1mWj4F3eT5fU;- z0wD-QB@#s@CnpgI2!_B5;U&L3aqsT_@9J{)HJQ1q(~5R*_WphIlrGI_f3?*{LN-z| z#I_`y%K*AT=ou0tLtuWD&|QnsGYJWMV^M~1%N$GM$O`ai5vJBAT6P;_TVf$A(8=-) z`(Vu+VZdH;yJ+PJ$8416nS#V=LBMhN+)>br{6|s2bQPXmiBDIcTiStM<~(d%4w0!S z>{kLzxF1$yOsfRFrjXQPm>yxz /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"