فهرست منبع

chore: auto bump version and changelog [skip ci]

iwt 3 ماه پیش
والد
کامیت
3af0887427

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CircleChallenge.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("circle_challenge")
+public class CircleChallenge implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long circleId;
+
+    private String challengeType;
+
+    private String title;
+
+    private String description;
+
+    private Integer durationDays;
+
+    private String period;
+
+    private Date startDate;
+
+    private Date endDate;
+
+    private String status;
+
+    private Date createdAt;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthCircle.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("health_circle")
+public class HealthCircle implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private String description;
+
+    private Long creatorFamilyId;
+
+    private String inviteCode;
+
+    private Integer memberLimit;
+
+    private String status;
+
+    private Date createdAt;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthCircleMember.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("health_circle_member")
+public class HealthCircleMember implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long circleId;
+
+    private Long familyId;
+
+    private String role;
+
+    private Date joinedAt;
+
+    private Date leftAt;
+
+    private String status;
+
+    private Date createdAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CircleChallengeMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CircleChallenge;
+
+public interface CircleChallengeMapper extends BaseMapper<CircleChallenge> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthCircleMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthCircle;
+
+public interface HealthCircleMapper extends BaseMapper<HealthCircle> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/HealthCircleMemberMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthCircleMember;
+
+public interface HealthCircleMemberMapper extends BaseMapper<HealthCircleMember> {
+}

+ 79 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CircleChallengeService.java

@@ -0,0 +1,79 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.CircleChallenge;
+import com.etotem.cfc.mapper.CircleChallengeMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class CircleChallengeService {
+
+    @Resource
+    private CircleChallengeMapper circleChallengeMapper;
+
+    public List<CircleChallenge> getActiveChallenges(Long circleId) {
+        if (circleId == null) return new ArrayList<>();
+        LambdaQueryWrapper<CircleChallenge> cw = new LambdaQueryWrapper<CircleChallenge>()
+                .eq(CircleChallenge::getCircleId, circleId)
+                .eq(CircleChallenge::getStatus, "active");
+        List<CircleChallenge> active = circleChallengeMapper.selectList(cw);
+        if (active.isEmpty()) {
+            autoCreateChallenges(circleId);
+            active = circleChallengeMapper.selectList(cw);
+        }
+        return active;
+    }
+
+    @Transactional
+    public void autoCreateChallenges(Long circleId) {
+        Calendar cal = Calendar.getInstance();
+        int year = cal.get(Calendar.YEAR);
+        int month = cal.get(Calendar.MONTH);
+        int day = cal.get(Calendar.DAY_OF_MONTH);
+
+        String[][] templates = {
+            {"weekly_checkin", "全员达标赛", "圈内家庭全员打卡≥5天", "7", "weekly"},
+            {"biweekly_sports", "运动总动员", "圈内运动时长前3家庭", "14", "biweekly"},
+            {"monthly_rising", "健康新秀榜", "圈内积分增长最快家庭", "30", "monthly"}
+        };
+
+        for (String[] t : templates) {
+            LambdaQueryWrapper<CircleChallenge> exists = new LambdaQueryWrapper<CircleChallenge>()
+                    .eq(CircleChallenge::getCircleId, circleId)
+                    .eq(CircleChallenge::getChallengeType, t[0])
+                    .eq(CircleChallenge::getStatus, "active");
+            if (circleChallengeMapper.selectCount(exists) > 0) continue;
+
+            CircleChallenge challenge = new CircleChallenge();
+            challenge.setCircleId(circleId);
+            challenge.setChallengeType(t[0]);
+            challenge.setTitle(t[1]);
+            challenge.setDescription(t[2]);
+            challenge.setDurationDays(Integer.parseInt(t[3]));
+            challenge.setPeriod(t[4]);
+            challenge.setStartDate(new Date());
+            cal = Calendar.getInstance();
+            cal.add(Calendar.DAY_OF_YEAR, Integer.parseInt(t[3]));
+            challenge.setEndDate(cal.getTime());
+            challenge.setStatus("active");
+            challenge.setCreatedAt(new Date());
+            circleChallengeMapper.insert(challenge);
+            log.info("已自动创建圈挑战: circleId={}, type={}", circleId, t[0]);
+        }
+    }
+
+    public List<CircleChallenge> getChallengeHistory(Long circleId) {
+        if (circleId == null) return new ArrayList<>();
+        LambdaQueryWrapper<CircleChallenge> cw = new LambdaQueryWrapper<CircleChallenge>()
+                .eq(CircleChallenge::getCircleId, circleId)
+                .eq(CircleChallenge::getStatus, "completed")
+                .orderByDesc(CircleChallenge::getEndDate);
+        return circleChallengeMapper.selectList(cw);
+    }
+}

+ 208 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/HealthCircleService.java

@@ -0,0 +1,208 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.CircleChallenge;
+import com.etotem.cfc.entity.HealthCircle;
+import com.etotem.cfc.entity.HealthCircleMember;
+import com.etotem.cfc.mapper.CircleChallengeMapper;
+import com.etotem.cfc.mapper.HealthCircleMapper;
+import com.etotem.cfc.mapper.HealthCircleMemberMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Slf4j
+@Service
+public class HealthCircleService {
+
+    @Resource
+    private HealthCircleMapper healthCircleMapper;
+
+    @Resource
+    private HealthCircleMemberMapper healthCircleMemberMapper;
+
+    @Resource
+    private CircleChallengeMapper circleChallengeMapper;
+
+    @Resource
+    private FamilyHealthScoreService familyHealthScoreService;
+
+    public List<HealthCircle> getUserCircles(Long familyId) {
+        if (familyId == null) return new ArrayList<>();
+        LambdaQueryWrapper<HealthCircleMember> mw = new LambdaQueryWrapper<HealthCircleMember>()
+                .eq(HealthCircleMember::getFamilyId, familyId)
+                .eq(HealthCircleMember::getStatus, "active");
+        List<HealthCircleMember> memberships = healthCircleMemberMapper.selectList(mw);
+        if (memberships.isEmpty()) return new ArrayList<>();
+
+        List<Long> circleIds = new ArrayList<>();
+        for (HealthCircleMember m : memberships) {
+            circleIds.add(m.getCircleId());
+        }
+        LambdaQueryWrapper<HealthCircle> cw = new LambdaQueryWrapper<HealthCircle>()
+                .in(HealthCircle::getId, circleIds)
+                .eq(HealthCircle::getStatus, "active");
+        return healthCircleMapper.selectList(cw);
+    }
+
+    @Transactional
+    public Result<HealthCircle> createCircle(String name, String description, Long creatorFamilyId) {
+        if (name == null || name.trim().isEmpty()) {
+            return Result.error("圈名称不能为空");
+        }
+        int count = countActiveCircles(creatorFamilyId);
+        if (count >= 3) {
+            return Result.error("每个家庭最多加入3个健康圈");
+        }
+
+        HealthCircle circle = new HealthCircle();
+        circle.setName(name.trim());
+        circle.setDescription(description);
+        circle.setCreatorFamilyId(creatorFamilyId);
+        circle.setInviteCode(generateInviteCode());
+        circle.setMemberLimit(20);
+        circle.setStatus("active");
+        circle.setCreatedAt(new Date());
+        healthCircleMapper.insert(circle);
+
+        HealthCircleMember owner = new HealthCircleMember();
+        owner.setCircleId(circle.getId());
+        owner.setFamilyId(creatorFamilyId);
+        owner.setRole("owner");
+        owner.setJoinedAt(new Date());
+        owner.setStatus("active");
+        owner.setCreatedAt(new Date());
+        healthCircleMemberMapper.insert(owner);
+
+        log.info("已创建健康圈: id={}, name={}, familyId={}", circle.getId(), name, creatorFamilyId);
+        return Result.success(circle);
+    }
+
+    @Transactional
+    public Result<Void> joinCircle(String inviteCode, Long familyId) {
+        if (inviteCode == null || familyId == null) {
+            return Result.error("参数错误");
+        }
+        LambdaQueryWrapper<HealthCircle> cw = new LambdaQueryWrapper<HealthCircle>()
+                .eq(HealthCircle::getInviteCode, inviteCode)
+                .eq(HealthCircle::getStatus, "active");
+        HealthCircle circle = healthCircleMapper.selectOne(cw);
+        if (circle == null) {
+            return Result.error("邀请码无效或圈已解散");
+        }
+
+        int count = countActiveCircles(familyId);
+        if (count >= 3) {
+            return Result.error("每个家庭最多加入3个健康圈");
+        }
+
+        LambdaQueryWrapper<HealthCircleMember> mw = new LambdaQueryWrapper<HealthCircleMember>()
+                .eq(HealthCircleMember::getCircleId, circle.getId())
+                .eq(HealthCircleMember::getFamilyId, familyId)
+                .eq(HealthCircleMember::getStatus, "active");
+        if (healthCircleMemberMapper.selectCount(mw) > 0) {
+            return Result.error("已在圈中");
+        }
+
+        int memberCount = countCircleMembers(circle.getId());
+        if (memberCount >= circle.getMemberLimit()) {
+            return Result.error("圈已满员");
+        }
+
+        HealthCircleMember member = new HealthCircleMember();
+        member.setCircleId(circle.getId());
+        member.setFamilyId(familyId);
+        member.setRole("member");
+        member.setJoinedAt(new Date());
+        member.setStatus("active");
+        member.setCreatedAt(new Date());
+        healthCircleMemberMapper.insert(member);
+
+        log.info("家庭加入健康圈: familyId={}, circleId={}", familyId, circle.getId());
+        return Result.success();
+    }
+
+    @Transactional
+    public Result<Void> leaveCircle(Long circleId, Long familyId) {
+        LambdaQueryWrapper<HealthCircleMember> mw = new LambdaQueryWrapper<HealthCircleMember>()
+                .eq(HealthCircleMember::getCircleId, circleId)
+                .eq(HealthCircleMember::getFamilyId, familyId)
+                .eq(HealthCircleMember::getStatus, "active");
+        HealthCircleMember membership = healthCircleMemberMapper.selectOne(mw);
+        if (membership == null) {
+            return Result.error("不在该圈中");
+        }
+        membership.setStatus("left");
+        membership.setLeftAt(new Date());
+        healthCircleMemberMapper.updateById(membership);
+        log.info("家庭退出健康圈: familyId={}, circleId={}", familyId, circleId);
+        return Result.success();
+    }
+
+    public Map<String, Object> getCircleRanking(Long circleId) {
+        Map<String, Object> result = new HashMap<>();
+        if (circleId == null) return result;
+
+        LambdaQueryWrapper<HealthCircleMember> mw = new LambdaQueryWrapper<HealthCircleMember>()
+                .eq(HealthCircleMember::getCircleId, circleId)
+                .eq(HealthCircleMember::getStatus, "active");
+        List<HealthCircleMember> members = healthCircleMemberMapper.selectList(mw);
+
+        List<Map<String, Object>> familyScores = new ArrayList<>();
+        for (HealthCircleMember m : members) {
+            Map<String, Object> entry = familyHealthScoreService.getFamilyRanking(m.getFamilyId(), null, "parent");
+            int totalScore = 0;
+            if (entry != null && entry.get("totalScore") != null) {
+                totalScore = ((Number) entry.get("totalScore")).intValue();
+            }
+            Map<String, Object> familyEntry = new HashMap<>();
+            familyEntry.put("familyId", m.getFamilyId());
+            familyEntry.put("totalScore", totalScore);
+            familyEntry.put("memberCount", entry.get("members") != null ? ((List) entry.get("members")).size() : 0);
+            familyScores.add(familyEntry);
+        }
+
+        familyScores.sort((a, b) -> ((Number) b.get("totalScore")).intValue() - ((Number) a.get("totalScore")).intValue());
+
+        HealthCircle circle = healthCircleMapper.selectById(circleId);
+        result.put("circleName", circle != null ? circle.getName() : "");
+        result.put("rankings", familyScores);
+        return result;
+    }
+
+    public List<CircleChallenge> getCircleChallenges(Long circleId) {
+        if (circleId == null) return new ArrayList<>();
+        LambdaQueryWrapper<CircleChallenge> cw = new LambdaQueryWrapper<CircleChallenge>()
+                .eq(CircleChallenge::getCircleId, circleId)
+                .orderByDesc(CircleChallenge::getCreatedAt);
+        return circleChallengeMapper.selectList(cw);
+    }
+
+    private int countActiveCircles(Long familyId) {
+        LambdaQueryWrapper<HealthCircleMember> mw = new LambdaQueryWrapper<HealthCircleMember>()
+                .eq(HealthCircleMember::getFamilyId, familyId)
+                .eq(HealthCircleMember::getStatus, "active");
+        return (int) healthCircleMemberMapper.selectCount(mw);
+    }
+
+    private int countCircleMembers(Long circleId) {
+        LambdaQueryWrapper<HealthCircleMember> mw = new LambdaQueryWrapper<HealthCircleMember>()
+                .eq(HealthCircleMember::getCircleId, circleId)
+                .eq(HealthCircleMember::getStatus, "active");
+        return (int) healthCircleMemberMapper.selectCount(mw);
+    }
+
+    private String generateInviteCode() {
+        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+        StringBuilder code = new StringBuilder();
+        Random rnd = new Random();
+        for (int i = 0; i < 6; i++) {
+            code.append(chars.charAt(rnd.nextInt(chars.length())));
+        }
+        return code.toString();
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-db00c867be3b7309be5ab4076536d677aa6fe6f0
+269a6ebfaa249880c7eda6aab6a630c42b1e3873