Browse Source

fix(成长打卡): 语音/图片打卡无需强制文字内容

Sisyphus Agent 1 day ago
parent
commit
89ade8855f

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

@@ -11326,6 +11326,18 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         } catch (Exception e) {
             log.warn("预置onboarding_guide_config可能已存在: {}", e.getMessage());
         }
+        // 迁移328: 饮食/运动打卡表补充照片与语音字段(前端已上传 media URL,补上持久化列)
+        try {
+            ensureColumn("health_meal_record", "photo_urls", "TEXT COMMENT '照片URL列表(逗号分隔)'");
+            ensureColumn("health_meal_record", "voice_url", "VARCHAR(500) COMMENT '语音URL'");
+            ensureColumn("health_meal_record", "voice_text", "TEXT COMMENT '语音转写文字'");
+            ensureColumn("exercise_record", "photo_urls", "TEXT COMMENT '照片URL列表(逗号分隔)'");
+            ensureColumn("exercise_record", "voice_url", "VARCHAR(500) COMMENT '语音URL'");
+            ensureColumn("exercise_record", "voice_text", "TEXT COMMENT '语音转写文字'");
+            log.info("迁移328: 已为health_meal_record/exercise_record补充照片语音字段");
+        } catch (Exception e) {
+            log.warn("迁移328: 补充照片语音字段失败: {}", e.getMessage());
+        }
     }
 
     /**

+ 6 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java

@@ -64,7 +64,12 @@ public class HealthExerciseController {
         }
         if (record.getMemberId() == null) return Result.error("memberId不能为空");
         if (record.getExerciseType() == null || record.getExerciseType().trim().isEmpty()) return Result.error("请选择运动类型");
-        if (record.getDurationMinutes() == null || record.getDurationMinutes() <= 0) return Result.error("运动时长需大于0分钟");
+        // 内容校验:运动时长、图片、语音 至少有一项即可
+        boolean hasContent = (record.getDurationMinutes() != null && record.getDurationMinutes() > 0)
+                || (record.getPhotoUrls() != null && !record.getPhotoUrls().trim().isEmpty())
+                || (record.getVoiceUrl() != null && !record.getVoiceUrl().trim().isEmpty())
+                || (record.getVoiceText() != null && !record.getVoiceText().trim().isEmpty());
+        if (!hasContent) return Result.error("请填写运动时长或添加图片/语音");
         record.setCreatedAt(new Date());
         healthExerciseRecordMapper.insert(record);
                 // 触发画像重算

+ 7 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java

@@ -209,7 +209,13 @@ public class HealthMealController {
         }
         if (record.getMemberId() == null) return Result.error("memberId不能为空");
         if (record.getMealType() == null || record.getMealType().trim().isEmpty()) return Result.error("请选择餐次");
-        if (record.getFoodItems() == null || record.getFoodItems().trim().isEmpty()) return Result.error("请填写食物清单");
+        // 内容校验:文字(食物清单/备注)、图片、语音 至少有一项即可
+        boolean hasContent = (record.getFoodItems() != null && !record.getFoodItems().trim().isEmpty())
+                || (record.getRemark() != null && !record.getRemark().trim().isEmpty())
+                || (record.getPhotoUrls() != null && !record.getPhotoUrls().trim().isEmpty())
+                || (record.getVoiceUrl() != null && !record.getVoiceUrl().trim().isEmpty())
+                || (record.getVoiceText() != null && !record.getVoiceText().trim().isEmpty());
+        if (!hasContent) return Result.error("请填写内容或添加图片/语音");
 
         // 补打逻辑
         boolean isBackfill = record.getIsBackfill() != null && record.getIsBackfill() == 1;

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthExerciseRecord.java

@@ -24,5 +24,11 @@ public class HealthExerciseRecord implements Serializable {
     private Date startTime;
     private Date endTime;
     private String remark;
+    /** 照片URL列表(逗号分隔) */
+    private String photoUrls;
+    /** 语音URL */
+    private String voiceUrl;
+    /** 语音转写文字 */
+    private String voiceText;
     private Date createdAt;
 }

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthMealRecord.java

@@ -36,6 +36,15 @@ public class HealthMealRecord {
 
     private String remark;
 
+    /** 照片URL列表(逗号分隔) */
+    private String photoUrls;
+
+    /** 语音URL */
+    private String voiceUrl;
+
+    /** 语音转写文字 */
+    private String voiceText;
+
     /** 用餐日期(补打时为实际用餐日,null=等于创建日) */
     private java.sql.Date recordDate;
 

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

@@ -3270,6 +3270,9 @@ CREATE TABLE IF NOT EXISTS health_meal_record (
     fiber_g INT COMMENT '膳食纤维克数',
     meal_time DATETIME COMMENT '用餐时间',
     remark VARCHAR(500) COMMENT '备注',
+    photo_urls TEXT COMMENT '照片URL列表(逗号分隔)',
+    voice_url VARCHAR(500) COMMENT '语音URL',
+    voice_text TEXT COMMENT '语音转写文字',
     record_date DATE COMMENT '用餐日期(补打时为实际用餐日,默认等于创建日)',
     is_backfill TINYINT DEFAULT 0 COMMENT '是否补打: 1=补打(无能量值), 0=当日打卡',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -3305,6 +3308,9 @@ CREATE TABLE IF NOT EXISTS exercise_record (
     start_time DATETIME COMMENT '开始时间',
     end_time DATETIME COMMENT '结束时间',
     remark TEXT COMMENT '备注',
+    photo_urls TEXT COMMENT '照片URL列表(逗号分隔)',
+    voice_url VARCHAR(500) COMMENT '语音URL',
+    voice_text TEXT COMMENT '语音转写文字',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     INDEX idx_member_id (member_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动打卡记录';

+ 21 - 2
cfc-frontend/pages/growth/diet-checkin.vue

@@ -144,6 +144,7 @@ export default {
     return {
       memberId: null,
       submitting: false,
+      pendingSubmit: false,
       isRecording: false,
       recordSeconds: 0,
       recordTimer: null,
@@ -205,9 +206,12 @@ export default {
       sttTranscribe(res.tempFilePath).then(function(text) {
         self.form.voiceText = text
         uni.hideLoading()
+        // 保存时停止录音 → 语音已就绪,继续提交
+        if (self.pendingSubmit) { self.pendingSubmit = false; self.doCheckin() }
       }).catch(function(err) {
         uni.hideLoading()
         console.warn('STT 转写失败:', err)
+        if (self.pendingSubmit) { self.pendingSubmit = false; self.doCheckin() }
       })
     })
     this.recorderManager.onError((err) => {
@@ -315,10 +319,25 @@ export default {
         })
       })
     },
-    async doCheckin() {
+    doCheckin: function() {
+      var self = this
+      if (self.submitting) return
+      // 保存时正在录音 → 先停止录音,等待 onStop 完成语音后继续提交
+      if (self.isRecording) {
+        self.pendingSubmit = true
+        self.recorderManager.stop()
+        return
+      }
+      self.submitCheckin()
+    },
+    submitCheckin: async function() {
       var self = this
       if (!self.form.mealType) { uni.showToast({ title: '请选择餐次', icon: 'none' }); return }
-      if (!self.form.foodItems || !self.form.foodItems.trim()) { uni.showToast({ title: '请填写食物清单', icon: 'none' }); return }
+      // 内容校验:文字(食物清单/备注)、图片、语音 至少有一项
+      var hasText = (self.form.foodItems && self.form.foodItems.trim()) || (self.form.remark && self.form.remark.trim())
+      var hasPhoto = self.form.photos && self.form.photos.length > 0
+      var hasVoice = self.form.voicePath || (self.form.voiceText && self.form.voiceText.trim())
+      if (!hasText && !hasPhoto && !hasVoice) { uni.showToast({ title: '请填写内容或添加图片/语音', icon: 'none' }); return }
       if (!self.memberId) { uni.showToast({ title: '孩子ID无效', icon: 'none' }); return }
       self.submitting = true
       try {

+ 22 - 3
cfc-frontend/pages/growth/exercise-checkin.vue

@@ -163,6 +163,7 @@ export default {
       memberId: null,
       isSelfCheckin: false,
       submitting: false,
+      pendingSubmit: false,
       syncingSteps: false,
       syncedStepText: '',
       isRecording: false,
@@ -228,9 +229,12 @@ export default {
       sttTranscribe(res.tempFilePath).then(function(text) {
         self.form.voiceText = text
         uni.hideLoading()
+        // 保存时停止录音 → 语音已就绪,继续提交
+        if (self.pendingSubmit) { self.pendingSubmit = false; self.doCheckin() }
       }).catch(function(err) {
         uni.hideLoading()
         console.warn('STT 转写失败:', err)
+        if (self.pendingSubmit) { self.pendingSubmit = false; self.doCheckin() }
       })
     })
     this.recorderManager.onError(() => {
@@ -401,11 +405,26 @@ export default {
         })
       })
     },
-    async doCheckin() {
+    doCheckin: function() {
+      var self = this
+      if (self.submitting) return
+      // 保存时正在录音 → 先停止录音,等待 onStop 完成语音后继续提交
+      if (self.isRecording) {
+        self.pendingSubmit = true
+        self.recorderManager.stop()
+        return
+      }
+      self.submitCheckin()
+    },
+    submitCheckin: async function() {
       var self = this
       if (!self.form.exerciseType) { uni.showToast({ title: '请选择运动类型', icon: 'none' }); return }
+      // 内容校验:运动时长、图片、语音 至少有一项
       var durationNum = parseInt(self.form.durationMinutes)
-      if (!self.form.durationMinutes || isNaN(durationNum) || durationNum <= 0) { uni.showToast({ title: '请填写有效的运动时长', icon: 'none' }); return }
+      var hasDuration = self.form.durationMinutes && !isNaN(durationNum) && durationNum > 0
+      var hasPhoto = self.form.photos && self.form.photos.length > 0
+      var hasVoice = self.form.voicePath || (self.form.voiceText && self.form.voiceText.trim())
+      if (!hasDuration && !hasPhoto && !hasVoice) { uni.showToast({ title: '请填写运动时长或添加图片/语音', icon: 'none' }); return }
       if (!self.memberId) { uni.showToast({ title: '孩子ID无效', icon: 'none' }); return }
       self.submitting = true
       try {
@@ -427,7 +446,7 @@ export default {
           data: {
             memberId: self.memberId,
             exerciseType: self.form.exerciseType,
-            durationMinutes: parseInt(self.form.durationMinutes),
+            durationMinutes: self.form.durationMinutes ? parseInt(self.form.durationMinutes) : null,
             intensity: self.form.intensity,
             caloriesBurned: self.form.caloriesBurned ? parseInt(self.form.caloriesBurned) : null,
             remark: self.form.remark || '',