|
|
@@ -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());
|
|
|
}
|
|
|
|