Explorar el Código

feat(health): 照片自动情绪识别 — Dify AI 图像分析 + 前端接入

- 新增 EmotionRecognitionResult DTO
- AIService.sendEmotionRecognition(): 调用 Dify 情绪识别 Workflow,支持 mock 兜底
- EmotionCheckinService.analyzeEmotionFromImage(): 解析结构化情绪列表
- EmotionCheckinController: 新增 POST /api/mind/checkin/emotion/recognize
- application.yml: 新增 dify.emotion-api-key 配置项
iwt hace 1 mes
padre
commit
2a52a6a5d9

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/mind/EmotionCheckinController.java

@@ -84,4 +84,19 @@ public class EmotionCheckinController {
         Object result = emotionCheckinService.getWeeklyReport(memberId);
         return Result.success(result);
     }
+
+    @PostMapping("/emotion/recognize")
+    public Result<?> recognizeEmotion(@RequestBody Map<String, Object> params) {
+        String imageUrl = params.get("image_url") != null ? params.get("image_url").toString() : "";
+        if (imageUrl.isEmpty()) {
+            return Result.error("image_url 不能为空");
+        }
+        try {
+            List<Map<String, Object>> emotions = emotionCheckinService.analyzeEmotionFromImage(imageUrl);
+            return Result.success(emotions);
+        } catch (Exception e) {
+            log.error("情绪识别失败: {}", e.getMessage());
+            return Result.error(e.getMessage());
+        }
+    }
 }

+ 77 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AIService.java

@@ -51,6 +51,9 @@ public class AIService {
     @Value("${dify.tongue-api-key:}")
     private String tongueApiKey;
 
+    @Value("${dify.emotion-api-key:}")
+    private String emotionApiKey;
+
     @Resource
     private EmotionCheckinMapper emotionCheckinMapper;
 
@@ -82,6 +85,13 @@ public class AIService {
         return headers;
     }
 
+    private HttpHeaders emotionAuthHeaders() {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setBearerAuth(emotionApiKey);
+        return headers;
+    }
+
     /**
      * 发送聊天消息: 统一走 Python LangGraph(不含 Dify 备选)
      */
@@ -320,6 +330,73 @@ public class AIService {
         return result;
     }
 
+    /**
+     * 照片情绪识别
+     * 调用 Dify 情绪识别 Workflow,输入图片 URL,返回结构化情绪列表
+     */
+    @SuppressWarnings("unchecked")
+    public Map<String, Object> sendEmotionRecognition(String imageUrl) {
+        if (emotionApiKey == null || emotionApiKey.isEmpty()) {
+            return mockEmotionResult();
+        }
+
+        String url = difyBaseUrl + "/workflows/run";
+        HttpHeaders headers = emotionAuthHeaders();
+
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("inputs", Collections.emptyMap());
+        body.put("user", "emotion-recognition");
+        body.put("response_mode", "blocking");
+
+        // 将图片转为 Dify 可接收的格式(URL)
+        Map<String, Object> fileInput = new HashMap<>();
+        fileInput.put("type", "image");
+        fileInput.put("url", imageUrl);
+        Map<String, Object> inputsWithPhoto = new LinkedHashMap<>(); inputsWithPhoto.put("photo", fileInput); body.put("inputs", inputsWithPhoto);
+
+        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
+        try {
+            ResponseEntity<Map> resp = restTemplate.postForEntity(url, entity, Map.class);
+            Map<String, Object> respBody = resp.getBody();
+            if (respBody != null && respBody.containsKey("data")) {
+                Map<String, Object> data = (Map<String, Object>) respBody.get("data");
+                Map<String, Object> outputs = (Map<String, Object>) data.get("outputs");
+                if (outputs != null) {
+                    return outputs;
+                }
+            }
+            return mockEmotionResult();
+        } catch (Exception e) {
+            log.warn("Dify emotion recognition failed, using mock: {}", e.getMessage());
+            return mockEmotionResult();
+        }
+    }
+
+    /**
+     * 模拟情绪识别结果(开发阶段使用)
+     */
+    private Map<String, Object> mockEmotionResult() {
+        Map<String, Object> result = new HashMap<>();
+        List<Map<String, Object>> emotions = new ArrayList<>();
+        String[][] mockData = {
+            {"开心", "joy", "60", "#10B981"},
+            {"平静", "trust", "25", "#3B82F6"},
+            {"疲惫", "sadness", "10", "#9CA3AF"},
+            {"期待", "anticipation", "5", "#F59E0B"}
+        };
+        for (String[] item : mockData) {
+            Map<String, Object> em = new HashMap<>();
+            em.put("label", item[0]);
+            em.put("type", item[1]);
+            em.put("confidence", Double.parseDouble(item[2]));
+            em.put("color", item[3]);
+            emotions.add(em);
+        }
+        result.put("emotions", emotions);
+        result.put("mood_weather", "sunny");
+        return result;
+    }
+
     /**
      * Build an emotional context prompt for the Dify AI conversation
      * based on the child's recent checkin history.

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EmotionCheckinService.java

@@ -37,6 +37,9 @@ public class EmotionCheckinService {
     @Resource
     private EmotionAlertService emotionAlertService;
 
+    @Resource
+    private AIService aiService;
+
     @Transactional
     public EmotionCheckinVO createCheckin(EmotionCheckinDTO dto, Long userId) {
         FamilyMember child = familyMemberMapper.selectById(dto.getMemberId());
@@ -356,4 +359,24 @@ public class EmotionCheckinService {
         vo.setSleepQuality(ec.getSleepQuality());
         return vo;
     }
+
+    /**
+     * 分析照片中的情绪,返回结构化情绪识别结果
+     */
+    public List<Map<String, Object>> analyzeEmotionFromImage(String imageUrl) {
+        try {
+            Map<String, Object> result = aiService.sendEmotionRecognition(imageUrl);
+            if (result == null || result.isEmpty()) {
+                return Collections.emptyList();
+            }
+            Object emotionsObj = result.get("emotions");
+            if (emotionsObj instanceof List) {
+                return (List<Map<String, Object>>) emotionsObj;
+            }
+            return Collections.emptyList();
+        } catch (Exception e) {
+            log.warn("情绪识别失败: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
 }

+ 1 - 0
cfc-backend/src/main/resources/application.yml

@@ -92,6 +92,7 @@ dify:
   api-key: ${DIFY_API_KEY:app-dev-only-key}
   nutrition-api-key: ${DIFY_NUTRITION_API_KEY:app-dev-only-nutrition-key}
   tongue-api-key: ""  # 舌诊分析,空字符串=mock模式
+  emotion-api-key: "" # 照片情绪识别,空字符串=mock模式
   callback-secret: ${DIFY_CALLBACK_SECRET:dev-only-callback-secret}
 
 python: