Просмотр исходного кода

feat(backend): 微行动×五维联动 — micro_actions 增加维度字段与短板维度推荐

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 1 месяц назад
Родитель
Сommit
86219568c6

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8596,5 +8596,20 @@ private void runMigration100() {
 		ensureColumn("consignees", "city", "VARCHAR(50) DEFAULT NULL COMMENT '城市'");
 		ensureColumn("consignees", "district", "VARCHAR(50) DEFAULT NULL COMMENT '区县'");
 		ensureColumn("consignees", "street", "VARCHAR(100) DEFAULT NULL COMMENT '详细地址'");
+
+		// 迁移213: micro_actions 添加维度字段(微行动×五维自检联动:按短板维度推荐今日行动)
+		ensureColumn("micro_actions", "dimension_code", "VARCHAR(32) DEFAULT NULL COMMENT '维度code: body/wisdom/wealth/action/mind'");
+		// 为既有12个行动补充维度映射(身体类→body,情绪类→mind)
+		try {
+			jdbcTemplate.execute("UPDATE micro_actions SET dimension_code = CASE code " +
+				"WHEN 'DRINK_WATER' THEN 'body' WHEN 'DEEP_BREATH' THEN 'mind' WHEN 'STRETCH' THEN 'body' " +
+				"WHEN 'LOOK_FAR' THEN 'body' WHEN 'SMILE' THEN 'mind' WHEN 'NECK_ROLL' THEN 'body' " +
+				"WHEN 'FIST_RELAX' THEN 'body' WHEN 'SIP_WATER' THEN 'body' WHEN 'BLINK' THEN 'body' " +
+				"WHEN 'ACUPRESSURE' THEN 'body' WHEN 'TIPTOE' THEN 'body' WHEN 'SHRUG' THEN 'body' " +
+				"ELSE dimension_code END WHERE dimension_code IS NULL OR dimension_code = ''");
+			log.info("迁移213: 已为既有微行动补充维度映射");
+		} catch (Exception e) {
+			log.warn("迁移213: 微行动维度映射更新失败: " + e.getMessage());
+		}
 	}
 }

+ 10 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/MicroActionController.java

@@ -30,7 +30,7 @@ public class MicroActionController {
         return Result.success(microActionService.getAllActions());
     }
 
-    @Operation(summary = "获取今日推荐微行动")
+    @Operation(summary = "获取今日推荐微行动(支持按维度指定)")
     @PostMapping("/today")
     public Result<Map<String, Object>> today(
             @RequestBody Map<String, Object> params,
@@ -38,12 +38,20 @@ public class MicroActionController {
             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long memberId = params.get("memberId") != null
                 ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
-        MicroAction action = microActionService.getTodayAction(memberId);
+        String dimensionCode = params.get("dimensionCode") != null
+                ? params.get("dimensionCode").toString() : null;
+        MicroAction action;
+        if (dimensionCode != null && !dimensionCode.isEmpty()) {
+            action = microActionService.getTodayActionForDimension(memberId, dimensionCode);
+        } else {
+            action = microActionService.getTodayAction(memberId);
+        }
         boolean completed = memberId != null && microActionService.hasCompletedToday(memberId);
 
         Map<String, Object> result = new HashMap<>();
         result.put("action", action);
         result.put("completed", completed);
+        result.put("dimensionCode", dimensionCode);
         return Result.success(result);
     }
 

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MicroAction.java

@@ -33,5 +33,8 @@ public class MicroAction implements Serializable {
     /** 状态: 1启用 0禁用 */
     private Integer status;
 
+    /** 维度code: body/wisdom/wealth/action/mind(微行动×五维自检联动) */
+    private String dimensionCode;
+
     private Date createdAt;
 }

+ 83 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/MicroActionService.java

@@ -3,6 +3,7 @@ package com.etotem.cfc.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -27,6 +28,12 @@ public class MicroActionService {
     @Resource
     private FamilyMemberMapper familyMemberMapper;
 
+    @Resource
+    private FiveDimensionSelfCheckMapper fiveDimensionSelfCheckMapper;
+
+    @Resource
+    private ObjectMapper objectMapper;
+
     @Resource
     private TaskMapper taskMapper;
 
@@ -58,7 +65,7 @@ public class MicroActionService {
 
     /**
      * 获取今日推荐的微行动
-     * 如果有未使用的新手体验配额,返回随机行动
+     * 优先按最近一次五维自检的短板维度推荐,无自检记录时按日期取模
      * 如果已完成今日配额,返回null
      */
     public MicroAction getTodayAction(Long childId) {
@@ -71,6 +78,13 @@ public class MicroActionService {
         List<MicroAction> actions = getAllActions();
         if (actions.isEmpty()) return null;
 
+        // 优先按最近自检短板维度推荐(微行动×五维联动)
+        String weakDim = getWeakestDimension(childId);
+        if (weakDim != null) {
+            MicroAction byDim = getActionByDimension(weakDim);
+            if (byDim != null) return byDim;
+        }
+
         // Pick one based on the day of month (consistent for the whole day)
         Calendar cal = Calendar.getInstance();
         int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
@@ -78,6 +92,74 @@ public class MicroActionService {
         return actions.get(index);
     }
 
+    /**
+     * 获取今日指定维度的微行动(自检结果页"加入今日微行动")
+     * 该维度无对应行动时回退到全量推荐;已完成今日配额返回null
+     */
+    public MicroAction getTodayActionForDimension(Long childId, String dimensionCode) {
+        if (getTodayRecord(childId) != null) {
+            return null; // Already completed today
+        }
+        if (dimensionCode != null && !dimensionCode.isEmpty()) {
+            MicroAction byDim = getActionByDimension(dimensionCode);
+            if (byDim != null) return byDim;
+        }
+        List<MicroAction> actions = getAllActions();
+        if (actions.isEmpty()) return null;
+        Calendar cal = Calendar.getInstance();
+        int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
+        return actions.get(dayOfMonth % actions.size());
+    }
+
+    /**
+     * 按维度code获取启用的微行动(当日取模保证整天一致)
+     */
+    public MicroAction getActionByDimension(String dimensionCode) {
+        if (dimensionCode == null || dimensionCode.isEmpty()) return null;
+        List<MicroAction> byDim = microActionMapper.selectList(
+            new LambdaQueryWrapper<MicroAction>()
+                .eq(MicroAction::getStatus, 1)
+                .eq(MicroAction::getDimensionCode, dimensionCode)
+                .orderByAsc(MicroAction::getSortOrder)
+        );
+        if (byDim.isEmpty()) return null;
+        Calendar cal = Calendar.getInstance();
+        return byDim.get(cal.get(Calendar.DAY_OF_MONTH) % byDim.size());
+    }
+
+    /**
+     * 解析最近一次五维自检,返回最低分(短板)维度code;无记录时返回null
+     */
+    private String getWeakestDimension(Long childId) {
+        if (childId == null) return null;
+        try {
+            FiveDimensionSelfCheck latest = fiveDimensionSelfCheckMapper.selectOne(
+                new LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                    .eq(FiveDimensionSelfCheck::getFamilyMemberId, childId)
+                    .orderByDesc(FiveDimensionSelfCheck::getId)
+                    .last("LIMIT 1")
+            );
+            if (latest == null || latest.getScoresJson() == null) return null;
+            Map<String, Integer> scores = objectMapper.readValue(latest.getScoresJson(),
+                objectMapper.getTypeFactory().constructMapType(Map.class, String.class, Integer.class));
+            // 五行相生顺序:身土→智金→富水→行木→心火
+            String[] order = {"body", "wisdom", "wealth", "action", "mind"};
+            String weakest = null;
+            int minScore = Integer.MAX_VALUE;
+            for (String dim : order) {
+                Integer s = scores.get(dim);
+                if (s != null && s < minScore) {
+                    minScore = s;
+                    weakest = dim;
+                }
+            }
+            return weakest;
+        } catch (Exception e) {
+            log.warn("解析自检短板维度失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
     /**
      * 检查今日是否已完成微行动
      */

+ 3 - 1
cfc-backend/src/main/resources/schema.sql

@@ -3336,9 +3336,11 @@ CREATE TABLE IF NOT EXISTS micro_actions (
     description VARCHAR(200) COMMENT '行动描述',
     sort_order INT DEFAULT 0 COMMENT '排序号',
     status TINYINT DEFAULT 1 COMMENT '状态: 1启用 0禁用',
+    dimension_code VARCHAR(32) COMMENT '维度code: body/wisdom/wealth/action/mind',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     UNIQUE KEY uk_code (code),
-    INDEX idx_status (status)
+    INDEX idx_status (status),
+    INDEX idx_dimension (dimension_code)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微行动定义(12个1秒健康行动)';
 
 -- 微行动完成记录表