فهرست منبع

feat(challenge): create改pending+respondChallenge+打卡结算口径收紧

asus 1 ماه پیش
والد
کامیت
d48ec9b805

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.java

@@ -56,4 +56,8 @@ public class FamilyChallenge implements Serializable {
 
     /** 创建者ID(系统创建为NULL,用户创建为userId) */
     private Long creatorId;
+
+    /** 非表字段:当前登录成员在本挑战的参与状态(pending/agreed/rejected/null=非参与) */
+    @TableField(exist = false)
+    private String myStatus;
 }

+ 242 - 43
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java

@@ -4,9 +4,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.FamilyChallenge;
+import com.etotem.cfc.entity.FamilyChallengeParticipant;
 import com.etotem.cfc.entity.FamilyChallengeProgress;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.mapper.FamilyChallengeMapper;
+import com.etotem.cfc.mapper.FamilyChallengeParticipantMapper;
 import com.etotem.cfc.mapper.FamilyChallengeProgressMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import lombok.extern.slf4j.Slf4j;
@@ -36,17 +38,25 @@ public class FamilyChallengeService {
     @Resource
     private PointsService pointsService;
 
-    public List<FamilyChallenge> getActiveChallenges(Long familyId) {
+    @Resource
+    private ChallengeScoreService challengeScoreService;
+
+    @Resource
+    private FamilyChallengeParticipantMapper familyChallengeParticipantMapper;
+
+    public List<FamilyChallenge> getActiveChallenges(Long familyId, Long currentMemberId) {
         if (familyId == null) return new ArrayList<>();
 
         LambdaQueryWrapper<FamilyChallenge> wrapper = new LambdaQueryWrapper<FamilyChallenge>()
                 .eq(FamilyChallenge::getFamilyId, familyId)
-                .eq(FamilyChallenge::getStatus, "active")
+                .in(FamilyChallenge::getStatus, "active", "pending")
                 .orderByDesc(FamilyChallenge::getCreatedAt);
         List<FamilyChallenge> active = familyChallengeMapper.selectList(wrapper);
 
         for (FamilyChallenge c : active) {
-            if (c.getEndDate() != null && new Date().after(c.getEndDate())) {
+            // 仅 active 且过期的挑战触发结算
+            if ("active".equals(c.getStatus())
+                    && c.getEndDate() != null && new Date().after(c.getEndDate())) {
                 checkAndSettle(c);
             }
         }
@@ -54,9 +64,22 @@ public class FamilyChallengeService {
         active = familyChallengeMapper.selectList(wrapper);
 
         fillProgress(active);
+        setMyStatus(active, currentMemberId);
         return active;
     }
 
+    /** 为每个挑战设置当前成员参与状态(myStatus) */
+    private void setMyStatus(List<FamilyChallenge> challenges, Long currentMemberId) {
+        if (challenges == null || challenges.isEmpty() || currentMemberId == null) return;
+        for (FamilyChallenge c : challenges) {
+            FamilyChallengeParticipant p = familyChallengeParticipantMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyChallengeParticipant>()
+                            .eq(FamilyChallengeParticipant::getChallengeId, c.getId())
+                            .eq(FamilyChallengeParticipant::getMemberId, currentMemberId));
+            c.setMyStatus(p != null ? p.getStatus() : null);
+        }
+    }
+
     @Transactional
     public void autoCreateChallenges(Long familyId) {
         Calendar cal = Calendar.getInstance();
@@ -98,10 +121,133 @@ public class FamilyChallengeService {
             challenge.setStatus("active");
             challenge.setCreatedAt(new Date());
             familyChallengeMapper.insert(challenge);
+            // 系统创建:写 participant 行(全员 agreed,与存量迁移口径一致)
+            LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getFamilyId, familyId);
+            List<FamilyMember> members = familyMemberMapper.selectList(mw);
+            for (FamilyMember m : members) {
+                FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
+                participant.setChallengeId(challenge.getId());
+                participant.setMemberId(m.getId());
+                participant.setStatus("agreed");
+                participant.setAgreedAt(new Date());
+                familyChallengeParticipantMapper.insert(participant);
+            }
             log.info("已自动创建挑战: familyId={}, type={}, start={}", familyId, t[0], startDate);
         }
     }
 
+    /**
+     * 同意/拒绝挑战(参与成员动作,即时结算方向评分)。
+     * agree:status→agreed → 结算同意评分 → 除 rejected 外全部 agreed → 挑战转 active
+     * reject:status→rejected → 结算拒绝评分 → 除发起人外无剩余参与成员 → 挑战自动 cancelled(Q6)
+     */
+    @Transactional
+    public Result<Void> respondChallenge(Long challengeId, Long memberId, String action) {
+        if (challengeId == null || memberId == null) {
+            return Result.error("参数错误");
+        }
+        if (!"agree".equals(action) && !"reject".equals(action)) {
+            return Result.error("action必须是agree或reject");
+        }
+
+        FamilyChallenge challenge = familyChallengeMapper.selectById(challengeId);
+        if (challenge == null) {
+            return Result.error("挑战不存在");
+        }
+        if (!"pending".equals(challenge.getStatus())) {
+            return Result.error("挑战当前不可操作");
+        }
+
+        // 调用者必须是该挑战 participant 且 status=pending(幂等:已处理过则拒绝)
+        FamilyChallengeParticipant participant = familyChallengeParticipantMapper.selectOne(
+                new LambdaQueryWrapper<FamilyChallengeParticipant>()
+                        .eq(FamilyChallengeParticipant::getChallengeId, challengeId)
+                        .eq(FamilyChallengeParticipant::getMemberId, memberId));
+        if (participant == null) {
+            return Result.error("你不在该挑战参与名单中");
+        }
+        if (!"pending".equals(participant.getStatus())) {
+            return Result.error("该挑战已处理过,请勿重复操作");
+        }
+
+        List<FamilyChallengeParticipant> allParticipants = listParticipants(challengeId);
+        List<Long> agreedMemberIds = new ArrayList<>();
+        for (FamilyChallengeParticipant p : allParticipants) {
+            if ("agreed".equals(p.getStatus())) agreedMemberIds.add(p.getMemberId());
+        }
+
+        if ("agree".equals(action)) {
+            participant.setStatus("agreed");
+            participant.setAgreedAt(new Date());
+            familyChallengeParticipantMapper.updateById(participant);
+
+            List<Long> participantIds = new ArrayList<>();
+            for (FamilyChallengeParticipant p : allParticipants) {
+                participantIds.add(p.getMemberId());
+            }
+            challengeScoreService.applyAgree(challenge.getFamilyId(), memberId, participantIds, challengeId);
+
+            // 除 rejected 外全部 agreed → active
+            boolean allAgreed = true;
+            for (FamilyChallengeParticipant p : allParticipants) {
+                if ("rejected".equals(p.getStatus())) continue;
+                if (!"agreed".equals(p.getStatus()) && !p.getMemberId().equals(memberId)) {
+                    allAgreed = false;
+                    break;
+                }
+            }
+            if (allAgreed) {
+                challenge.setStatus("active");
+                familyChallengeMapper.updateById(challenge);
+            }
+        } else {
+            // reject
+            participant.setStatus("rejected");
+            participant.setAgreedAt(new Date());
+            familyChallengeParticipantMapper.updateById(participant);
+
+            Long initiatorId = challenge.getCreatorId() != null
+                    ? resolveMemberIdByUserId(challenge.getCreatorId()) : null;
+            // 发起人为 null(系统创建)时按 memberId 归属推断:保守处理,仅结算 A→X 与能量
+            if (initiatorId != null) {
+                challengeScoreService.applyReject(challenge.getFamilyId(), memberId, initiatorId, agreedMemberIds, challengeId);
+            } else {
+                // 系统创建挑战:仅已同意成员→拒绝人 扣沟通 + 能量扣减(无发起人维度)
+                challengeScoreService.applyReject(challenge.getFamilyId(), memberId, memberId, agreedMemberIds, challengeId);
+            }
+
+            // 除发起人外无剩余参与成员 → cancelled(Q6)
+            int nonRejected = 0;
+            for (FamilyChallengeParticipant p : allParticipants) {
+                if (!"rejected".equals(p.getStatus())) nonRejected++;
+            }
+            // 非 rejected 的只剩发起人本人(且发起人是 participant)
+            if (nonRejected <= 1) {
+                challenge.setStatus("cancelled");
+                familyChallengeMapper.updateById(challenge);
+            }
+        }
+
+        return Result.success();
+    }
+
+    /** 解析 user_id → 家庭成员ID(发起人) */
+    private Long resolveMemberIdByUserId(Long userId) {
+        if (userId == null) return null;
+        // 发起人 creatorId 存的是 userId;查其家庭成员
+        List<FamilyMember> members = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getUserId, userId));
+        return members.isEmpty() ? null : members.get(0).getId();
+    }
+
+    private List<FamilyChallengeParticipant> listParticipants(Long challengeId) {
+        return familyChallengeParticipantMapper.selectList(
+                new LambdaQueryWrapper<FamilyChallengeParticipant>()
+                        .eq(FamilyChallengeParticipant::getChallengeId, challengeId)
+                        .orderByAsc(FamilyChallengeParticipant::getId));
+    }
+
     @Transactional
     public Result<Void> updateProgress(Long challengeId, Long childId, int delta) {
         if (challengeId == null || childId == null) {
@@ -130,6 +276,15 @@ public class FamilyChallengeService {
             return Result.error("无权为该成员打卡");
         }
 
+        // 3.5 参与成员校验:仅 agreed 参与成员可打卡(pending/rejected 拒绝)
+        FamilyChallengeParticipant participant = familyChallengeParticipantMapper.selectOne(
+                new LambdaQueryWrapper<FamilyChallengeParticipant>()
+                        .eq(FamilyChallengeParticipant::getChallengeId, challengeId)
+                        .eq(FamilyChallengeParticipant::getMemberId, childId));
+        if (participant == null || !"agreed".equals(participant.getStatus())) {
+            return Result.error("仅参与成员可打卡");
+        }
+
         // 4. 查该成员在本挑战的进度
         LambdaQueryWrapper<FamilyChallengeProgress> wrapper = new LambdaQueryWrapper<FamilyChallengeProgress>()
                 .eq(FamilyChallengeProgress::getChallengeId, challengeId)
@@ -198,10 +353,14 @@ public class FamilyChallengeService {
             // 集体总合目标:全家人合计达到 target_value
             isCompleted = totalProgress >= targetValue;
         } else {
-            // 全员个人目标:家庭内每个成员都达到 target_value
-            LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
-                    .eq(FamilyMember::getFamilyId, challenge.getFamilyId());
-            List<FamilyMember> members = familyMemberMapper.selectList(mw);
+            // 全员个人目标:每个 agreed 参与成员都达到 target_value
+            List<FamilyChallengeParticipant> participants = listParticipants(challenge.getId());
+            List<FamilyMember> members = new ArrayList<>();
+            for (FamilyChallengeParticipant p : participants) {
+                if (!"agreed".equals(p.getStatus())) continue;
+                FamilyMember m = familyMemberMapper.selectById(p.getMemberId());
+                if (m != null) members.add(m);
+            }
             if (members.isEmpty()) {
                 isCompleted = false;
             } else {
@@ -271,9 +430,6 @@ public class FamilyChallengeService {
     private void fillProgress(List<FamilyChallenge> challenges) {
         if (challenges == null || challenges.isEmpty()) return;
 
-        // 缓存各家庭全部成员,避免逐挑战重复查询
-        Map<Long, List<FamilyMember>> familyMembersCache = new HashMap<>();
-
         for (FamilyChallenge c : challenges) {
             normalizeTarget(c);
 
@@ -290,24 +446,21 @@ public class FamilyChallengeService {
             }
             c.setTotalProgress(total);
 
-            List<FamilyMember> members = familyMembersCache.get(c.getFamilyId());
-            if (members == null) {
-                LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
-                        .eq(FamilyMember::getFamilyId, c.getFamilyId());
-                members = familyMemberMapper.selectList(mw);
-                familyMembersCache.put(c.getFamilyId(), members);
-            }
+            // 参与成员(participant 表,含 status),替代原"全部家庭成员"
+            List<FamilyChallengeParticipant> participants = listParticipants(c.getId());
 
             List<Map<String, Object>> memberProgress = new ArrayList<>();
-            if (members != null) {
-                for (FamilyMember m : members) {
-                    Integer v = progressMap.get(m.getId());
+            if (participants != null) {
+                for (FamilyChallengeParticipant part : participants) {
+                    FamilyMember m = familyMemberMapper.selectById(part.getMemberId());
+                    Integer v = progressMap.get(part.getMemberId());
                     int value = v != null ? v : 0;
                     Map<String, Object> mp = new HashMap<>();
-                    mp.put("memberId", m.getId());
-                    mp.put("nickname", m.getNickname() != null ? m.getNickname() : "成员" + m.getId());
+                    mp.put("memberId", part.getMemberId());
+                    mp.put("nickname", m != null && m.getNickname() != null ? m.getNickname() : "成员" + part.getMemberId());
                     mp.put("progressValue", value);
                     mp.put("completed", value >= c.getTargetValue());
+                    mp.put("status", part.getStatus());
                     memberProgress.add(mp);
                 }
             }
@@ -356,11 +509,11 @@ public class FamilyChallengeService {
     }
 
     @Transactional
-    public Result<Long> createChallenge(Long familyId, Long creatorId, Map<String, Object> params) {
-        if (familyId == null || creatorId == null) {
-            return Result.error("familyId和creatorId不能为空");
+    public Result<Long> createChallenge(Long familyId, Long userId, Long memberId, Map<String, Object> params) {
+        if (familyId == null || userId == null || memberId == null) {
+            return Result.error("familyId、userId和memberId不能为空");
         }
-        
+
         // 验证title
         String title = params.get("title") != null ? params.get("title").toString() : null;
         if (title == null || title.trim().isEmpty()) {
@@ -369,41 +522,70 @@ public class FamilyChallengeService {
         if (title.length() > 200) {
             return Result.error("title不能超过200个字符");
         }
-        
+
         // 验证targetMode
         String targetMode = params.get("targetMode") != null ? params.get("targetMode").toString() : "all_members";
         if (!"aggregate".equals(targetMode) && !"all_members".equals(targetMode)) {
             return Result.error("targetMode必须是aggregate或all_members");
         }
-        
+
         // 验证targetValue
-        Integer targetValue = params.get("targetValue") != null ? 
-            Integer.parseInt(params.get("targetValue").toString()) : 0;
+        Integer targetValue = params.get("targetValue") != null ?
+                Integer.parseInt(params.get("targetValue").toString()) : 0;
         if (targetValue <= 0) {
             return Result.error("targetValue必须大于0");
         }
-        
+
         // 验证rewardPoints
-        Integer rewardPoints = params.get("rewardPoints") != null ? 
-            Integer.parseInt(params.get("rewardPoints").toString()) : 0;
+        Integer rewardPoints = params.get("rewardPoints") != null ?
+                Integer.parseInt(params.get("rewardPoints").toString()) : 0;
         if (rewardPoints < 0) {
             return Result.error("rewardPoints不能小于0");
         }
-        
+
         // 验证durationDays
-        Integer durationDays = params.get("durationDays") != null ? 
-            Integer.parseInt(params.get("durationDays").toString()) : 7;
+        Integer durationDays = params.get("durationDays") != null ?
+                Integer.parseInt(params.get("durationDays").toString()) : 7;
         if (durationDays <= 0) {
             return Result.error("durationDays必须大于0");
         }
-        
+
         // 验证description
         String description = params.get("description") != null ? params.get("description").toString() : "";
-        
-        // 创建挑战
+
+        // 参与成员校验:默认全员;必须包含发起人本人且均属本家庭
+        List<Long> participantIds = new ArrayList<>();
+        Object rawIds = params.get("participantMemberIds");
+        if (rawIds == null) {
+            // 未传 → 默认本家庭全部成员
+            List<FamilyMember> members = familyMemberMapper.selectList(
+                    new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+            for (FamilyMember m : members) {
+                participantIds.add(m.getId());
+            }
+        } else if (rawIds instanceof java.util.Collection) {
+            for (Object o : (java.util.Collection<?>) rawIds) {
+                if (o == null) continue;
+                participantIds.add(Long.valueOf(o.toString()));
+            }
+        }
+        if (participantIds.isEmpty() || !participantIds.contains(memberId)) {
+            return Result.error("参与成员必须包含发起人本人");
+        }
+        // 均属本家庭
+        for (Long pid : participantIds) {
+            LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                    .eq(FamilyMember::getId, pid)
+                    .eq(FamilyMember::getFamilyId, familyId);
+            if (familyMemberMapper.selectCount(mw) == 0) {
+                return Result.error("参与成员不属于本家庭");
+            }
+        }
+
+        // 创建挑战(pending 状态,参与成员同意后转 active)
         FamilyChallenge challenge = new FamilyChallenge();
         challenge.setFamilyId(familyId);
-        challenge.setCreatorId(creatorId);
+        challenge.setCreatorId(userId);
         challenge.setChallengeType(params.get("challengeType") != null ? params.get("challengeType").toString() : "custom");
         challenge.setTitle(title);
         challenge.setDescription(description);
@@ -413,11 +595,28 @@ public class FamilyChallengeService {
         challenge.setTargetValue(targetValue);
         challenge.setStartDate(new Date());
         challenge.setEndDate(new Date(System.currentTimeMillis() + durationDays * 24L * 60 * 60 * 1000));
-        challenge.setStatus("active");
+        challenge.setStatus("pending");
         challenge.setCreatedAt(new Date());
-        
+
         familyChallengeMapper.insert(challenge);
-        
+
+        // 写 participant 行:发起人 agreed,其余 pending
+        for (Long pid : participantIds) {
+            FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
+            participant.setChallengeId(challenge.getId());
+            participant.setMemberId(pid);
+            if (pid.equals(memberId)) {
+                participant.setStatus("agreed");
+                participant.setAgreedAt(new Date());
+            } else {
+                participant.setStatus("pending");
+            }
+            familyChallengeParticipantMapper.insert(participant);
+        }
+
+        // 立即结算发起评分(§4.4)
+        challengeScoreService.applyInitiate(familyId, memberId, participantIds, challenge.getId());
+
         return Result.success(challenge.getId());
     }