Kaynağa Gözat

fix(activity): P1-3 评价门槛与去重——已报名已签到提交;同一 childId 更新不新增

- 评价前校验:活动已结束且用户已报名通过且已签到(检查Task status=completed)
- Dedup:同一 activityId + childId 只保留一条;重复提交更新而不新增
- 状态统一:新增 submitted/checked_in 时统一设置 status=1(已发布)
- Entity 加 updated_at 字段 + schema.sql + 迁移392 同步 UNIQUE KEY(活动+孩子)
E2E Test Bot 3 hafta önce
ebeveyn
işleme
5c4abe4840

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

@@ -3170,6 +3170,15 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 迁移391: activity_registrations 添加取消时间/原因列(P0-2 取消报名支持 approved + 退款链路,取消时记录)
         ensureColumn("activity_registrations", "cancelled_at", "DATETIME DEFAULT NULL COMMENT '取消报名时间'");
         ensureColumn("activity_registrations", "cancel_reason", "VARCHAR(200) DEFAULT NULL COMMENT '取消原因'");
+
+        // 迁移392: activity_feedback 添加 updated_at 列 + activity_id+child_id 唯一索引(P1-3 评价去重:同一孩子只能评一次,重复提交改为更新)
+        try {
+            ensureColumn("activity_feedback", "updated_at", "DATETIME DEFAULT NULL COMMENT '更新时间(重复提交评价时更新)'");
+            jdbcTemplate.execute("ALTER TABLE activity_feedback ADD UNIQUE KEY uk_activity_child (activity_id, child_id)");
+            log.info("迁移392: 已为 activity_feedback 添加 updated_at 列和 uk_activity_child 唯一索引");
+        } catch (Exception e) {
+            log.warn("迁移392: 添加 activity_feedback.updated_at / uk_activity_child 失败(可能已存在): {}", e.getMessage());
+        }
     }
 
     private void migrateHealthDimensionsTable() {

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

@@ -16,6 +16,7 @@ public class ActivityFeedback {
     private String comment;
     private Integer status;
     private Date createdAt;
+    private Date updatedAt;
 
     public Long getId() { return id; }
     public void setId(Long id) { this.id = id; }
@@ -33,4 +34,6 @@ public class ActivityFeedback {
     public void setStatus(Integer status) { this.status = status; }
     public Date getCreatedAt() { return createdAt; }
     public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
+    public Date getUpdatedAt() { return updatedAt; }
+    public void setUpdatedAt(Date updatedAt) { this.updatedAt = updatedAt; }
 }

+ 56 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityFeedbackService.java

@@ -1,12 +1,17 @@
 package com.etotem.cfc.service;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Activity;
 import com.etotem.cfc.entity.ActivityFeedback;
 import com.etotem.cfc.mapper.ActivityFeedbackMapper;
+import com.etotem.cfc.mapper.TaskMapper;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Date;
 import java.util.List;
 import com.etotem.cfc.util.SortUtil;
@@ -16,17 +21,59 @@ public class ActivityFeedbackService {
 
     @Resource
     private ActivityFeedbackMapper activityFeedbackMapper;
+    @Resource
+    private TaskMapper taskMapper;
+    @Resource
+    private ActivityRegistrationService registrationService;
+    @Resource
+    private ActivityService activityService;
 
     public Result<?> submit(Long activityId, Long userId, Long memberId, String ratings, String comment) {
-        ActivityFeedback feedback = new ActivityFeedback();
-        feedback.setActivityId(activityId);
-        feedback.setUserId(userId);
-        feedback.setChildId(memberId);
-        feedback.setRatings(ratings);
-        feedback.setComment(comment);
-        feedback.setStatus(0);
-        feedback.setCreatedAt(new Date());
-        activityFeedbackMapper.insert(feedback);
+        // Gate: 活动必须已结束(status=ended 或 endTime < now)
+        Activity activity = activityService.getById(activityId);
+        if (activity == null) return Result.error("活动不存在");
+        if (!"ended".equals(activity.getStatus())) {
+            Date now = new Date();
+            if (activity.getEndTime() != null && !now.after(activity.getEndTime())) {
+                return Result.error("活动尚未结束,暂不能评价");
+            }
+        }
+        // Gate: 已报名且已签到
+        if (memberId == null || !registrationService.isApproved(activityId, memberId)) {
+            return Result.error("请先报名此活动后再评价");
+        }
+        boolean hasCheckedIn = taskMapper.selectCount(new LambdaQueryWrapper<com.etotem.cfc.entity.Task>()
+                .eq(com.etotem.cfc.entity.Task::getSourceType, "activity")
+                .eq(com.etotem.cfc.entity.Task::getSourceId, activityId)
+                .eq(com.etotem.cfc.entity.Task::getChildId, memberId)
+                .eq(com.etotem.cfc.entity.Task::getStatus, "completed")) > 0;
+        if (!hasCheckedIn) {
+            return Result.error("请先完成签到后再评价");
+        }
+        // Dedup: 同一 activityId + childId 只保留一条;重复提交则更新
+        ActivityFeedback existing = activityFeedbackMapper.selectOne(new LambdaQueryWrapper<ActivityFeedback>()
+                .eq(ActivityFeedback::getActivityId, activityId)
+                .eq(ActivityFeedback::getChildId, memberId)
+                .last("LIMIT 1"));
+        ActivityFeedback feedback;
+        if (existing != null) {
+            feedback = existing;
+            feedback.setRatings(ratings);
+            feedback.setComment(comment);
+            feedback.setStatus(1);
+            feedback.setUpdatedAt(new Date());
+            activityFeedbackMapper.updateById(feedback);
+        } else {
+            feedback = new ActivityFeedback();
+            feedback.setActivityId(activityId);
+            feedback.setUserId(userId);
+            feedback.setChildId(memberId);
+            feedback.setRatings(ratings);
+            feedback.setComment(comment);
+            feedback.setStatus(1);
+            feedback.setCreatedAt(new Date());
+            activityFeedbackMapper.insert(feedback);
+        }
         return Result.success("提交评价成功", feedback.getId());
     }
 

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

@@ -3958,9 +3958,11 @@ CREATE TABLE IF NOT EXISTS activity_feedback (
     comment TEXT COMMENT '文字评价',
     status TINYINT DEFAULT 0 COMMENT '状态: 0=待审核 1=已发布',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT NULL COMMENT '更新时间(重复提交评价时更新)',
     INDEX idx_activity_id (activity_id),
     INDEX idx_user_id (user_id),
-    INDEX idx_child_id (child_id)
+    INDEX idx_child_id (child_id),
+    UNIQUE KEY uk_activity_child (activity_id, child_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动评价表';
 
 -- ============================================================