Przeglądaj źródła

feat: 章鱼图成效录入支持发生时间(happened_at)

Sisyphus Agent 1 miesiąc temu
rodzic
commit
bbbadd9014

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

@@ -10901,6 +10901,27 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
         } catch (Exception e) {
             log.warn("迁移313: 修复 meal_configs participant_member_ids 失败: {}", e.getMessage());
         }
+
+        // 迁移314: meal_configs 增加 config_date 字段(当天临时共餐配置)+ 修改 UNIQUE KEY
+        // 支持「今天」模式:config_date 有值=当天临时配置(优先级最高),config_date=NULL+config_date_type=工作日/周末默认配置
+        try {
+            jdbcTemplate.execute("ALTER TABLE meal_configs ADD COLUMN config_date DATE DEFAULT NULL COMMENT '当天临时配置日期(NULL=默认工作日/周末配置)'");
+            log.info("迁移314: meal_configs 已添加 config_date 列");
+        } catch (Exception e) {
+            log.warn("迁移314: meal_configs 添加 config_date 列失败(可能已存在): {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE meal_configs DROP INDEX uk_config");
+            log.info("迁移314: meal_configs 已删除旧 UNIQUE KEY uk_config");
+        } catch (Exception e) {
+            log.warn("迁移314: 删除旧 UNIQUE KEY 失败(可能不存在): {}", e.getMessage());
+        }
+        try {
+            jdbcTemplate.execute("ALTER TABLE meal_configs ADD UNIQUE KEY uk_config (family_id, COALESCE(config_date, '9999-12-31'), meal_type)");
+            log.info("迁移314: meal_configs 已添加新 UNIQUE KEY uk_config(支持当天临时配置)");
+        } catch (Exception e) {
+            log.warn("迁移314: 添加新 UNIQUE KEY 失败: {}", e.getMessage());
+        }
     }
 
     private void seedGutFloraFingerprints() {
@@ -10930,5 +10951,14 @@ sqls.add("INSERT IGNORE INTO health_norm_reference (dimension, gender, age_min,
         } catch (Exception e) {
             log.warn("迁移312: 写入 gut_flora 指纹规则失败: {}", e.getMessage());
         }
+
+        // 迁移313: octopus_effect_record 添加 happened_at 列(成效发生时间)
+        try {
+            jdbcTemplate.execute("ALTER TABLE octopus_effect_record ADD COLUMN happened_at DATETIME COMMENT '成效发生时间'");
+            log.info("迁移313: octopus_effect_record.happened_at 列已添加");
+        } catch (Exception e) {
+            // 列已存在,忽略
+            log.warn("迁移313: octopus_effect_record.happened_at 列已存在或添加失败: {}", e.getMessage());
+        }
     }
 }

+ 10 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/KeyPersonController.java

@@ -51,7 +51,16 @@ public class KeyPersonController {
             List<Map<String, Object>> list = (List<Map<String, Object>>) kpObj;
             keyPersons = list;
         }
-        return keyPersonService.addEffect(userId, memberOrderId, effectType, effectAmount, effectDesc, keyPersons);
+        // 解析发生时间(ISO 8601 字符串,如 "2026-09-10T14:30:00")
+        java.util.Date happenedAt = null;
+        Object timeObj = params.get("happenedAt");
+        if (timeObj != null && timeObj.toString().trim().length() > 0) {
+            try {
+                java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+                happenedAt = sdf.parse(timeObj.toString().trim());
+            } catch (Exception ignored) { /* 时间解析失败则忽略,使用系统时间 */ }
+        }
+        return keyPersonService.addEffect(userId, memberOrderId, effectType, effectAmount, effectDesc, keyPersons, happenedAt);
     }
 
     @PostMapping("/effect/delete")

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/MealConfig.java

@@ -14,6 +14,7 @@ public class MealConfig implements Serializable {
     private Long id;
     private Long familyId;
     private String configDateType;
+    private java.time.LocalDate configDate;
     private String mealType;
     private String participantMemberIds;
     private String notes;

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/OctopusEffectRecord.java

@@ -30,6 +30,8 @@ public class OctopusEffectRecord implements Serializable {
 
     private Date createdAt;
 
+    private Date happenedAt;
+
     // 非持久化:系统自动计算的标签列表(查询时动态计算)
     private transient java.util.List<String> effectTypeList;
 }

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

@@ -577,6 +577,7 @@ public class BeijingNutritionService {
         LambdaQueryWrapper<MealConfig> configWrapper = new LambdaQueryWrapper<>();
         configWrapper.eq(MealConfig::getFamilyId, familyId);
         configWrapper.eq(MealConfig::getConfigDateType, isWeekend(date) ? "weekend" : "weekday");
+        configWrapper.isNull(MealConfig::getConfigDate);
         SortUtil.applySort(configWrapper);
         List<MealConfig> mealConfigs = mealConfigMapper.selectList(configWrapper);
         
@@ -599,6 +600,28 @@ public class BeijingNutritionService {
         }
         
         // 未配置共餐时,降级为查询该家庭全部成员
+        // 当天无默认配置匹配时,优先查当天临时配置(config_date = 当天)
+        if (participantIds.isEmpty()) {
+            LambdaQueryWrapper<MealConfig> todayWrapper = new LambdaQueryWrapper<>();
+            todayWrapper.eq(MealConfig::getFamilyId, familyId);
+            todayWrapper.eq(MealConfig::getConfigDate, date);
+            SortUtil.applySort(todayWrapper);
+            List<MealConfig> todayConfigs = mealConfigMapper.selectList(todayWrapper);
+            for (MealConfig config : todayConfigs) {
+                if (config.getParticipantMemberIds() == null) continue;
+                try {
+                    com.fasterxml.jackson.databind.JsonNode nodes = new com.fasterxml.jackson.databind.ObjectMapper().readTree(config.getParticipantMemberIds());
+                    if (nodes.isArray()) {
+                        for (com.fasterxml.jackson.databind.JsonNode node : nodes) {
+                            participantIds.add(node.asLong());
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warn("解析当天临时配置 participantMemberIds 失败: {}", e.getMessage());
+                }
+            }
+        }
+
         if (participantIds.isEmpty()) {
             LambdaQueryWrapper<FamilyMember> memberWrapper = new LambdaQueryWrapper<>();
             memberWrapper.eq(FamilyMember::getFamilyId, familyId);

+ 104 - 17
cfc-backend/src/main/java/com/etotem/cfc/service/DietMealConfigService.java

@@ -18,14 +18,27 @@ public class DietMealConfigService {
     @Resource
     private MealConfigMapper mealConfigMapper;
 
+    /**
+     * 获取共餐配置
+     * dateType: "weekday"/"weekend" → 查默认配置;"today" → 查当天临时配置
+     */
     public Map<String, Object> getConfig(Long familyId, String dateType, String mealType) {
         LambdaQueryWrapper<MealConfig> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(MealConfig::getFamilyId, familyId)
-               .eq(MealConfig::getConfigDateType, dateType)
                .eq(MealConfig::getMealType, mealType);
-        
-        MealConfig config = mealConfigMapper.selectOne(wrapper);
-        
+
+        MealConfig config;
+        if ("today".equals(dateType)) {
+            // 当天临时配置:优先查当天日期
+            wrapper.eq(MealConfig::getConfigDate, LocalDate.now());
+            config = mealConfigMapper.selectOne(wrapper);
+        } else {
+            // 工作日/周末默认配置:config_date 必须为 NULL
+            wrapper.eq(MealConfig::getConfigDateType, dateType)
+                   .isNull(MealConfig::getConfigDate);
+            config = mealConfigMapper.selectOne(wrapper);
+        }
+
         Map<String, Object> result = new HashMap<>();
         if (config != null) {
             result.put("id", config.getId());
@@ -34,14 +47,20 @@ public class DietMealConfigService {
             result.put("mealType", config.getMealType());
             result.put("participantMemberIds", config.getParticipantMemberIds());
             result.put("notes", config.getNotes());
+            result.put("configDate", config.getConfigDate());
         } else {
             result.put("id", null);
             result.put("participantMemberIds", "[]");
         }
-        
+
         return result;
     }
 
+    /**
+     * 保存共餐配置
+     * dateType="today" 时保存当天临时配置(config_date=当天,config_date_type=NULL)
+     * dateType="weekday"/"weekend" 时保存默认配置(config_date=NULL)
+     */
     public void saveConfig(Long familyId, Map<String, Object> request) {
         String dateType = request.get("date_type") != null ? request.get("date_type").toString() : null;
         if (dateType == null || dateType.isEmpty()) {
@@ -50,7 +69,6 @@ public class DietMealConfigService {
 
         Object configsObj = request.get("configs");
         if (configsObj instanceof Map) {
-            // 批量格式: {"date_type":"weekday","configs":{"breakfast":"[1,2]","lunch":"[]","dinner":"[3]"}}
             Map<?, ?> configs = (Map<?, ?>) configsObj;
             for (Map.Entry<?, ?> entry : configs.entrySet()) {
                 if (entry.getKey() == null || entry.getValue() == null) {
@@ -61,7 +79,6 @@ public class DietMealConfigService {
             return;
         }
 
-        // 单条格式(兼容旧调用): {"date_type":"weekday","meal_type":"breakfast","participant_member_ids":"[1,2]","notes":"..."}
         String mealType = request.get("meal_type") != null ? request.get("meal_type").toString() : null;
         String participantMemberIds = request.get("participant_member_ids") != null
                 ? request.get("participant_member_ids").toString() : null;
@@ -77,26 +94,44 @@ public class DietMealConfigService {
         saveOneConfig(familyId, dateType, mealType, participantMemberIds, notes);
     }
 
-    private void saveOneConfig(Long familyId, String dateType, String mealType, String participantMemberIds, String notes) {
+    private void saveOneConfig(Long familyId, String dateType, String mealType,
+                               String participantMemberIds, String notes) {
+        saveOneConfig(familyId, dateType, mealType, participantMemberIds, notes, null);
+    }
+
+    private void saveOneConfig(Long familyId, String dateType, String mealType,
+                               String participantMemberIds, String notes, LocalDate configDate) {
         LambdaQueryWrapper<MealConfig> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(MealConfig::getFamilyId, familyId)
-               .eq(MealConfig::getConfigDateType, dateType)
                .eq(MealConfig::getMealType, mealType);
-        
+        if ("today".equals(dateType)) {
+            wrapper.eq(MealConfig::getConfigDate, configDate != null ? configDate : LocalDate.now());
+        } else {
+            wrapper.eq(MealConfig::getConfigDateType, dateType)
+                   .isNull(MealConfig::getConfigDate);
+        }
+
         MealConfig config = mealConfigMapper.selectOne(wrapper);
-        
+
         if (config == null) {
             config = new MealConfig();
             config.setFamilyId(familyId);
-            config.setConfigDateType(dateType);
             config.setMealType(mealType);
             config.setCreatedAt(new java.util.Date());
         }
-        
+
+        if ("today".equals(dateType)) {
+            config.setConfigDate(configDate != null ? configDate : LocalDate.now());
+            config.setConfigDateType(null);
+        } else {
+            config.setConfigDateType(dateType);
+            config.setConfigDate(null);
+        }
+
         config.setParticipantMemberIds(participantMemberIds);
         config.setNotes(notes);
         config.setUpdatedAt(new java.util.Date());
-        
+
         if (config.getId() == null) {
             mealConfigMapper.insert(config);
         } else {
@@ -104,17 +139,69 @@ public class DietMealConfigService {
         }
     }
 
+    /**
+     * 获取当天共餐参与者 IDs(优先当天临时配置,降级到工作日/周末默认配置)
+     */
+    public List<Long> queryTodayParticipantIds(Long familyId, LocalDate date) {
+        boolean isWeekend = date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
+        String defaultDateType = isWeekend ? "weekend" : "weekday";
+        Set<Long> ids = new HashSet<>();
+
+        // 优先查当天临时配置
+        for (String mealType : Arrays.asList("breakfast", "lunch", "dinner")) {
+            LambdaQueryWrapper<MealConfig> w = new LambdaQueryWrapper<>();
+            w.eq(MealConfig::getFamilyId, familyId)
+             .eq(MealConfig::getConfigDate, date)
+             .eq(MealConfig::getMealType, mealType);
+            MealConfig config = mealConfigMapper.selectOne(w);
+            if (config != null && config.getParticipantMemberIds() != null) {
+                ids.addAll(parseMemberIds(config.getParticipantMemberIds()));
+            }
+        }
+
+        // 当天无临时配置时,降级到工作日/周末默认配置
+        if (ids.isEmpty()) {
+            for (String mealType : Arrays.asList("breakfast", "lunch", "dinner")) {
+                LambdaQueryWrapper<MealConfig> w = new LambdaQueryWrapper<>();
+                w.eq(MealConfig::getFamilyId, familyId)
+                 .eq(MealConfig::getConfigDateType, defaultDateType)
+                 .isNull(MealConfig::getConfigDate)
+                 .eq(MealConfig::getMealType, mealType);
+                MealConfig config = mealConfigMapper.selectOne(w);
+                if (config != null && config.getParticipantMemberIds() != null) {
+                    ids.addAll(parseMemberIds(config.getParticipantMemberIds()));
+                }
+            }
+        }
+
+        return new ArrayList<>(ids);
+    }
+
+    private Set<Long> parseMemberIds(String json) {
+        Set<Long> ids = new HashSet<>();
+        if (json == null || json.isEmpty()) return ids;
+        try {
+            com.fasterxml.jackson.databind.JsonNode nodes = new com.fasterxml.jackson.databind.ObjectMapper().readTree(json);
+            if (nodes.isArray()) {
+                for (com.fasterxml.jackson.databind.JsonNode node : nodes) ids.add(node.asLong());
+            }
+        } catch (Exception e) {
+            log.warn("解析 participantMemberIds 失败: {}", e.getMessage());
+        }
+        return ids;
+    }
+
     public Map<String, Object> getMealSummary(Long familyId, LocalDate date) {
         boolean isWeekend = date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
         String dateType = isWeekend ? "weekend" : "weekday";
-        
+
         Map<String, Object> summary = new HashMap<>();
-        
+
         for (String mealType : Arrays.asList("breakfast", "lunch", "dinner")) {
             Map<String, Object> config = getConfig(familyId, dateType, mealType);
             summary.put(mealType + "_config", config);
         }
-        
+
         return summary;
     }
 }

+ 37 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/DietRecommendationService.java

@@ -207,28 +207,44 @@ public class DietRecommendationService {
     // 内部辅助
     // ============================================================
 
-    /** 查询共餐参与者(按工作日/周末的 meal_configs,未配置则降级为全部家庭成员) */
+    /**
+     * 查询共餐参与者
+     * 优先级:当天临时配置(config_date=当天) > 工作日/周末默认配置(config_date=NULL) > 全部家庭成员
+     */
     private List<Long> queryParticipantIds(Long familyId, LocalDate date) {
         boolean weekend = date.getDayOfWeek() == java.time.DayOfWeek.SATURDAY
                 || date.getDayOfWeek() == java.time.DayOfWeek.SUNDAY;
         String dateType = weekend ? "weekend" : "weekday";
         Set<Long> ids = new HashSet<>();
+
+        // 优先查当天临时配置(config_date = 当天)
         for (String mealType : Arrays.asList("breakfast", "lunch", "dinner")) {
             LambdaQueryWrapper<MealConfig> w = new LambdaQueryWrapper<>();
             w.eq(MealConfig::getFamilyId, familyId)
-             .eq(MealConfig::getConfigDateType, dateType)
+             .eq(MealConfig::getConfigDate, date)
              .eq(MealConfig::getMealType, mealType);
             MealConfig config = mealConfigMapper.selectOne(w);
             if (config != null && config.getParticipantMemberIds() != null) {
-                try {
-                    com.fasterxml.jackson.databind.JsonNode nodes = new com.fasterxml.jackson.databind.ObjectMapper()
-                            .readTree(config.getParticipantMemberIds());
-                    if (nodes.isArray()) {
-                        for (com.fasterxml.jackson.databind.JsonNode node : nodes) ids.add(node.asLong());
-                    }
-                } catch (Exception ignore) {}
+                ids.addAll(parseMemberIds(config.getParticipantMemberIds()));
             }
         }
+
+        // 当天无临时配置时,降级到工作日/周末默认配置
+        if (ids.isEmpty()) {
+            for (String mealType : Arrays.asList("breakfast", "lunch", "dinner")) {
+                LambdaQueryWrapper<MealConfig> w = new LambdaQueryWrapper<>();
+                w.eq(MealConfig::getFamilyId, familyId)
+                 .eq(MealConfig::getConfigDateType, dateType)
+                 .isNull(MealConfig::getConfigDate)
+                 .eq(MealConfig::getMealType, mealType);
+                MealConfig config = mealConfigMapper.selectOne(w);
+                if (config != null && config.getParticipantMemberIds() != null) {
+                    ids.addAll(parseMemberIds(config.getParticipantMemberIds()));
+                }
+            }
+        }
+
+        // 仍无配置时,降级为全部家庭成员
         if (ids.isEmpty()) {
             LambdaQueryWrapper<FamilyMember> w = new LambdaQueryWrapper<>();
             w.eq(FamilyMember::getFamilyId, familyId);
@@ -238,6 +254,18 @@ public class DietRecommendationService {
         return new ArrayList<>(ids);
     }
 
+    private Set<Long> parseMemberIds(String json) {
+        Set<Long> ids = new HashSet<>();
+        if (json == null || json.isEmpty()) return ids;
+        try {
+            com.fasterxml.jackson.databind.JsonNode nodes = new com.fasterxml.jackson.databind.ObjectMapper().readTree(json);
+            if (nodes.isArray()) {
+                for (com.fasterxml.jackson.databind.JsonNode node : nodes) ids.add(node.asLong());
+            }
+        } catch (Exception ignore) {}
+        return ids;
+    }
+
     /** 归一化 LangGraph 菜单 → 前端期望结构(dish.calories / ingredients[].amount / method) */
     private Map<String, Object> normalizeMenu(String menuJson, int participantCount) {
         Map<String, Object> result = new LinkedHashMap<>();

+ 3 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/KeyPersonService.java

@@ -135,7 +135,8 @@ public class KeyPersonService {
 
     public Result<OctopusEffectRecord> addEffect(Long userId, Long memberOrderId, Integer effectType,
                                                  BigDecimal effectAmount, String effectDesc,
-                                                 List<Map<String, Object>> keyPersons) {
+                                                 List<Map<String, Object>> keyPersons,
+                                                 java.util.Date happenedAt) {
         OctopusEffectRecord r = new OctopusEffectRecord();
         r.setUserId(userId);
         r.setMemberOrderId(memberOrderId);
@@ -144,6 +145,7 @@ public class KeyPersonService {
         r.setEffectDesc(effectDesc != null ? effectDesc.trim() : null);
         r.setStatus(1);
         r.setCreatedAt(new Date());
+        r.setHappenedAt(happenedAt);
         effectRecordMapper.insert(r);
 
         // 保存关键人关联

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

@@ -4606,18 +4606,20 @@ CREATE TABLE IF NOT EXISTS diet_preferences (
     INDEX idx_updated (updated_at)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食偏好调研表';
 
--- 共餐配置表(按家庭+日期类型+餐次)
+-- 共餐配置表(按家庭+日期类型+餐次;config_date非NULL时为当天临时配置,优先级高于默认配置)
 CREATE TABLE IF NOT EXISTS meal_configs (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     family_id BIGINT NOT NULL COMMENT '家庭ID',
     config_date_type VARCHAR(20) NOT NULL COMMENT '工作日/周末/节假日',
+    config_date DATE DEFAULT NULL COMMENT '当天临时配置日期(NULL=默认工作日/周末配置)',
     meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner',
     participant_member_ids JSON NOT NULL COMMENT '参与成员ID列表',
     notes VARCHAR(255) DEFAULT NULL COMMENT '备注',
     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    UNIQUE KEY uk_config (family_id, config_date_type, meal_type),
-    INDEX idx_family (family_id)
+    UNIQUE KEY uk_config (family_id, COALESCE(config_date, '9999-12-31'), meal_type),
+    INDEX idx_family (family_id),
+    INDEX idx_config_date (config_date)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='共餐配置表';
 
 -- 食谱推荐方案表
@@ -5280,6 +5282,7 @@ CREATE TABLE IF NOT EXISTS octopus_effect_record (
     effect_desc VARCHAR(500) COMMENT '成效描述',
     status TINYINT DEFAULT 1 COMMENT '1=有效,0=无效',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    happened_at DATETIME COMMENT '成效发生时间',
     INDEX idx_oer_user (user_id),
     INDEX idx_oer_order (member_order_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人拓展-成效记录';

+ 54 - 1
cfc-frontend/pages/action-detail/octopus-add-effect.vue

@@ -16,6 +16,20 @@
       <textarea class="desc-input" v-model="effectDesc" placeholder="简述成交来源,如:家长转介绍、社区活动等" maxlength="200"></textarea>
     </view>
 
+    <!-- 发生时间 -->
+    <view class="form-section">
+      <text class="section-label">发生时间(选填)</text>
+      <view class="datetime-row">
+        <picker mode="date" :value="happenedDate" @change="onDateChange">
+          <view class="form-picker">{{ happenedDate || '选择日期' }}</view>
+        </picker>
+        <text class="datetime-sep">·</text>
+        <picker mode="time" :value="happenedTime" @change="onTimeChange">
+          <view class="form-picker">{{ happenedTime || '选择时间' }}</view>
+        </picker>
+      </view>
+    </view>
+
     <!-- 关联关键人(可多选 + 角色标记) -->
     <view class="form-section">
       <view class="kp-header">
@@ -53,9 +67,17 @@ import { addOctopusEffect, getOctopusKeyPersons } from '../../utils/api.js'
 
 export default {
   data() {
+    var now = new Date()
+    var y = now.getFullYear()
+    var m = String(now.getMonth() + 1).padStart(2, '0')
+    var d = String(now.getDate()).padStart(2, '0')
+    var h = String(now.getHours()).padStart(2, '0')
+    var min = String(now.getMinutes()).padStart(2, '0')
     return {
       effectAmount: '',
       effectDesc: '',
+      happenedDate: y + '-' + m + '-' + d,
+      happenedTime: h + ':' + min,
       keyPersons: [],
       selectedMap: {},   // kpId -> true
       roleMap: {}        // kpId -> role (1引荐/2决策/3其他)
@@ -93,12 +115,22 @@ export default {
     setRole(id, role) {
       this.$set(this.roleMap, id, role)
     },
+    onDateChange(e) {
+      this.happenedDate = e.detail.value
+    },
+    onTimeChange(e) {
+      this.happenedTime = e.detail.value
+    },
     async onSubmit() {
       if (!this.effectAmount.trim()) {
         uni.showToast({ title: '请填写成交金额', icon: 'none' })
         return
       }
-      var data = { effectAmount: this.effectAmount, effectDesc: this.effectDesc.trim() }
+      var data = {
+        effectAmount: this.effectAmount,
+        effectDesc: this.effectDesc.trim(),
+        happenedAt: this.happenedDate + 'T' + this.happenedTime + ':00'
+      }
       // 组装关键人列表
       var kpList = []
       var ids = Object.keys(this.selectedMap).filter(function(k) { return this.selectedMap[k] }, this)
@@ -244,4 +276,25 @@ export default {
 .btn-submit[disabled] {
   background: #ccc;
 }
+.form-picker {
+  width: 45%;
+  height: 88rpx;
+  background: #fafafa;
+  border-radius: 12rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+  display: flex;
+  align-items: center;
+  box-sizing: border-box;
+}
+.datetime-row {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+.datetime-sep {
+  font-size: 28rpx;
+  color: #ccc;
+}
 </style>

+ 41 - 0
cfc-frontend/pages/diet/index.vue

@@ -208,8 +208,47 @@ export default {
   onShow: function() {
     // onLoad 已加载过,onShow 只刷新食材(共餐配置不重复请求)
     this.loadIngredients()
+    this.checkTodayMealConfig()
   },
   methods: {
+    checkTodayMealConfig: function() {
+      var self = this
+      // 未配置日常共餐时不提醒(页面已有「去设置共餐」引导)
+      if (!self.hasMealConfig) return
+      var todayKey = 'diet_today_config_prompt_' + self.formatDate(new Date())
+      if (uni.getStorageSync(todayKey)) return
+      Promise.all([
+        getMealConfig({ date_type: 'today', meal_type: 'breakfast' }),
+        getMealConfig({ date_type: 'today', meal_type: 'lunch' }),
+        getMealConfig({ date_type: 'today', meal_type: 'dinner' })
+      ]).then(function(results) {
+        var hasTodayConfig = false
+        results.forEach(function(r) {
+          if (r && r.code === 200 && r.data && r.data.id) {
+            hasTodayConfig = true
+          }
+        })
+        if (!hasTodayConfig) {
+          self.showMealConfigPrompt()
+        }
+      })
+    },
+    showMealConfigPrompt: function() {
+      var self = this
+      uni.showModal({
+        title: '调整今日共餐?',
+        content: '今天的共餐人和平时一样吗?不一样的话,可以为今天重新选择共餐人(只对今天生效)。',
+        confirmText: '重新选择',
+        cancelText: '一样,不用了',
+        success: function(modalRes) {
+          var todayKey = 'diet_today_config_prompt_' + self.formatDate(new Date())
+          uni.setStorageSync(todayKey, '1')
+          if (modalRes.confirm) {
+            self.navTo('/pages/diet/meal-config?date_type=today')
+          }
+        }
+      })
+    },
     navTo: function(url) {
       uni.navigateTo({ url: url })
     },
@@ -308,6 +347,8 @@ export default {
         self.mealSummary.breakfast = self.parseMembers(results[0])
         self.mealSummary.lunch = self.parseMembers(results[1])
         self.mealSummary.dinner = self.parseMembers(results[2])
+        // 日常配置加载完成后,检查是否需要提醒重新选择今天的共餐人
+        self.checkTodayMealConfig()
       })
     },
     parseMembers: function(result) {

+ 19 - 7
cfc-frontend/pages/diet/meal-config.vue

@@ -4,6 +4,9 @@
     <scroll-view class="content" scroll-y>
       <!-- 日期切换:两段式胶囊 tabs -->
       <view class="date-tabs">
+        <view class="tab-item" :class="{'tab-active': dateType === 'today'}" @tap="switchDateType('today')">
+          <text class="tab-text">今天</text>
+        </view>
         <view class="tab-item" :class="{'tab-active': dateType === 'weekday'}" @tap="switchDateType('weekday')">
           <text class="tab-text">工作日</text>
         </view>
@@ -44,7 +47,7 @@
 
       <!-- 提示条 -->
       <view class="tip-card">
-        <text class="tip-text">💡 提示:选择与您在{{ dateType === 'weekday' ? '工作日' : '周末' }}共餐的家庭成员</text>
+        <text class="tip-text">{{ tipText }}</text>
       </view>
 
       <!-- 保存按钮 -->
@@ -140,12 +143,21 @@ export default {
             self.$set(self.mealConfigs, meal.value, config)
           }
         })
-      })
-    },
-    switchDateType: function(type) {
-      this.dateType = type
-      this.loadConfigs()
-    },
+})
+  },
+  tipText: function() {
+    var dt = this.dateType
+    var label = dt === 'today' ? '今天' : (dt === 'weekday' ? '工作日' : '周末')
+    return '💡 提示:选择与您在' + label + '共餐的家庭成员'
+  }
+},
+onLoad: function(options) {
+  if (options && options.date_type === 'today') {
+    this.dateType = 'today'
+  }
+  this.loadMembers()
+  this.loadConfigs()
+},
     toggleMember: function(mealType, memberId) {
       var config = this.mealConfigs[mealType] || {}
       var ids = []