Просмотр исходного кода

feat(doa): 新增DoaService(创建阶段/家庭总览/详情/周计划/周复盘)

Sisyphus 1 неделя назад
Родитель
Сommit
f2647935f2
1 измененных файлов с 337 добавлено и 0 удалено
  1. 337 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DoaService.java

+ 337 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DoaService.java

@@ -0,0 +1,337 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DoaGoal;
+import com.etotem.cfc.entity.DoaStage;
+import com.etotem.cfc.entity.DoaWeek;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.mapper.DoaGoalMapper;
+import com.etotem.cfc.mapper.DoaStageMapper;
+import com.etotem.cfc.mapper.DoaWeekMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.util.SortUtil;
+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 DoaService {
+
+    @Resource
+    private DoaStageMapper doaStageMapper;
+
+    @Resource
+    private DoaGoalMapper doaGoalMapper;
+
+    @Resource
+    private DoaWeekMapper doaWeekMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    private static final Set<String> VALID_DOMAINS = new HashSet<>(Arrays.asList(
+            "health", "wealth", "family", "social", "help"));
+
+    /** 周起始:周一 */
+    private static Date weekStart(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        return cal.getTime();
+    }
+
+    private static Date weekEnd(Date weekStart) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(weekStart);
+        cal.add(Calendar.DAY_OF_YEAR, 6);
+        cal.set(Calendar.HOUR_OF_DAY, 23);
+        cal.set(Calendar.MINUTE, 59);
+        cal.set(Calendar.SECOND, 59);
+        cal.set(Calendar.MILLISECOND, 999);
+        return cal.getTime();
+    }
+
+    /**
+     * 创建阶段:校验归属 + 周期 + 目标数量,写 stage + goals,预生成 N 条 week。
+     *
+     * @param memberId 目标成员ID;null 表示当前登录成员自己
+     * @param creatorUserId 当前登录 userId
+     * @param familyId 当前家庭
+     */
+    @Transactional
+    public Result<Long> createStage(Long memberId, Long creatorUserId, Long familyId,
+                                    String title, Integer durationWeeks, List<Map<String, Object>> goals) {
+        if (creatorUserId == null || familyId == null) {
+            return Result.error("未登录或未加入家庭");
+        }
+        if (durationWeeks == null || (durationWeeks != 2 && durationWeeks != 4 && durationWeeks != 8 && durationWeeks != 12)) {
+            return Result.error("durationWeeks必须是2/4/8/12");
+        }
+        if (goals == null || goals.isEmpty() || goals.size() > 3) {
+            return Result.error("每阶段需设置1-3个目标");
+        }
+
+        // 目标成员:默认当前登录成员自己
+        if (memberId == null) {
+            memberId = resolveMemberIdByUserId(creatorUserId, familyId);
+            if (memberId == null) {
+                return Result.error("未找到当前成员身份");
+            }
+        }
+
+        // 校验目标成员属于当前家庭
+        FamilyMember targetMember = familyMemberMapper.selectById(memberId);
+        if (targetMember == null || !familyId.equals(targetMember.getFamilyId())) {
+            return Result.error("目标成员不属于当前家庭");
+        }
+
+        // 代管校验:memberId != 当前成员 时,目标成员必须是不能登录成员(userId 为空)
+        Long selfMemberId = resolveMemberIdByUserId(creatorUserId, familyId);
+        if (selfMemberId != null && !selfMemberId.equals(memberId)) {
+            if (targetMember.getUserId() != null) {
+                return Result.error("只能代管不能登录成员的目标");
+            }
+        }
+
+        // 校验目标领域与去重
+        Set<String> domains = new HashSet<>();
+        for (Map<String, Object> g : goals) {
+            String domain = g.get("domain") != null ? g.get("domain").toString() : "";
+            String content = g.get("content") != null ? g.get("content").toString().trim() : "";
+            if (!VALID_DOMAINS.contains(domain)) {
+                return Result.error("目标领域无效: " + domain);
+            }
+            if (content.isEmpty()) {
+                return Result.error("目标内容不能为空");
+            }
+            if (!domains.add(domain)) {
+                return Result.error("同一阶段目标领域不能重复: " + domain);
+            }
+        }
+
+        // 日期计算:start=今天,end=start + durationWeeks*7 - 1
+        Date startDate = new Date();
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(startDate);
+        cal.add(Calendar.DAY_OF_YEAR, durationWeeks * 7 - 1);
+        Date endDate = cal.getTime();
+
+        // 写 stage
+        DoaStage stage = new DoaStage();
+        stage.setFamilyId(familyId);
+        stage.setMemberId(memberId);
+        stage.setTitle(title);
+        stage.setStartDate(startDate);
+        stage.setEndDate(endDate);
+        stage.setDurationWeeks(durationWeeks);
+        stage.setStatus("active");
+        stage.setCreatedBy(creatorUserId);
+        stage.setCreatedAt(new Date());
+        doaStageMapper.insert(stage);
+
+        // 写 goals
+        int order = 0;
+        for (Map<String, Object> g : goals) {
+            DoaGoal goal = new DoaGoal();
+            goal.setStageId(stage.getId());
+            goal.setDomain(g.get("domain").toString());
+            goal.setContent(g.get("content").toString().trim());
+            goal.setStatus("active");
+            goal.setSortOrder(order++);
+            goal.setCreatedAt(new Date());
+            doaGoalMapper.insert(goal);
+        }
+
+        // 预生成 N 条 week(周一为一周起点,首周从 start 所在周的周一起算)
+        Date firstWeekStart = weekStart(stage.getStartDate());
+        for (int i = 1; i <= durationWeeks; i++) {
+            DoaWeek week = new DoaWeek();
+            week.setStageId(stage.getId());
+            week.setWeekNo(i);
+            Date ws = addDays(firstWeekStart, (i - 1) * 7);
+            week.setWeekStart(ws);
+            week.setWeekEnd(weekEnd(ws));
+            week.setStatus("pending");
+            week.setCreatedAt(new Date());
+            doaWeekMapper.insert(week);
+        }
+
+        log.info("DOA创建阶段: stageId={}, memberId={}, weeks={}, goals={}",
+                stage.getId(), memberId, durationWeeks, goals.size());
+        return Result.success(stage.getId());
+    }
+
+    /** 家庭 DOA 总览:各成员当前 active 阶段 */
+    public List<Map<String, Object>> getFamilyOverview(Long familyId) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (familyId == null) return result;
+
+        LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getFamilyId, familyId);
+        SortUtil.applySort(mw);
+        List<FamilyMember> members = familyMemberMapper.selectList(mw);
+
+        for (FamilyMember m : members) {
+            LambdaQueryWrapper<DoaStage> sw = new LambdaQueryWrapper<DoaStage>()
+                    .eq(DoaStage::getFamilyId, familyId)
+                    .eq(DoaStage::getMemberId, m.getId())
+                    .eq(DoaStage::getStatus, "active")
+                    .orderByDesc(DoaStage::getCreatedAt);
+            SortUtil.applySort(sw);
+            List<DoaStage> stages = doaStageMapper.selectList(sw);
+
+            Map<String, Object> memberMap = new HashMap<>();
+            memberMap.put("memberId", m.getId());
+            memberMap.put("nickname", m.getNickname());
+            memberMap.put("memberType", m.getUserId() == null ? "local" : "login");
+
+            if (!stages.isEmpty()) {
+                DoaStage current = stages.get(0);
+                current.setMemberUserId(m.getUserId());
+                fillStageDetail(current);
+                memberMap.put("currentStage", current);
+            }
+            result.add(memberMap);
+        }
+        return result;
+    }
+
+    /** 阶段详情(stage + goals + weeks) */
+    public Result<DoaStage> getStageDetail(Long stageId, Long familyId) {
+        if (stageId == null) return Result.error("stageId不能为空");
+        DoaStage stage = doaStageMapper.selectById(stageId);
+        if (stage == null) return Result.error("阶段不存在");
+        if (!familyId.equals(stage.getFamilyId())) return Result.error("无权查看该阶段");
+
+        fillStageDetail(stage);
+
+        // 成员昵称
+        FamilyMember m = familyMemberMapper.selectById(stage.getMemberId());
+        stage.setMemberNickname(m != null ? m.getNickname() : "成员" + stage.getMemberId());
+        stage.setMemberUserId(m != null ? m.getUserId() : null);
+        return Result.success(stage);
+    }
+
+    /** 填充 stage 的 goals + weeks(含复盘周数统计) */
+    private void fillStageDetail(DoaStage stage) {
+        LambdaQueryWrapper<DoaGoal> gw = new LambdaQueryWrapper<DoaGoal>()
+                .eq(DoaGoal::getStageId, stage.getId())
+                .orderByAsc(DoaGoal::getSortOrder);
+        SortUtil.applySort(gw);
+        stage.setGoals(doaGoalMapper.selectList(gw));
+
+        LambdaQueryWrapper<DoaWeek> ww = new LambdaQueryWrapper<DoaWeek>()
+                .eq(DoaWeek::getStageId, stage.getId())
+                .orderByAsc(DoaWeek::getWeekNo);
+        SortUtil.applySort(ww);
+        List<DoaWeek> weeks = doaWeekMapper.selectList(ww);
+        stage.setWeeks(weeks);
+        stage.setTotalWeeks(weeks.size());
+
+        int reviewed = 0;
+        for (DoaWeek w : weeks) {
+            if ("reviewed".equals(w.getStatus())) reviewed++;
+        }
+        stage.setReviewedWeeks(reviewed);
+    }
+
+    /** 取消阶段 */
+    @Transactional
+    public Result<Void> cancelStage(Long stageId, Long userId, Long familyId) {
+        DoaStage stage = doaStageMapper.selectById(stageId);
+        if (stage == null) return Result.error("阶段不存在");
+        if (!familyId.equals(stage.getFamilyId())) return Result.error("无权操作该阶段");
+        if (!"active".equals(stage.getStatus())) return Result.error("仅active阶段可取消");
+        // 权限:本人 或 代管不能登录成员
+        Long selfMemberId = resolveMemberIdByUserId(userId, familyId);
+        boolean isSelf = selfMemberId != null && selfMemberId.equals(stage.getMemberId());
+        FamilyMember target = familyMemberMapper.selectById(stage.getMemberId());
+        boolean canManageLocal = target != null && target.getUserId() == null;
+        if (!isSelf && !canManageLocal) {
+            return Result.error("无权取消该阶段");
+        }
+        stage.setStatus("cancelled");
+        doaStageMapper.updateById(stage);
+        return Result.success();
+    }
+
+    /** 提交周计划(A表) */
+    @Transactional
+    public Result<Void> submitWeekPlan(Long weekId, String plan, Long userId, Long familyId) {
+        DoaWeek week = doaWeekMapper.selectById(weekId);
+        if (week == null) return Result.error("周记录不存在");
+        DoaStage stage = doaStageMapper.selectById(week.getStageId());
+        if (stage == null || !familyId.equals(stage.getFamilyId())) return Result.error("无权操作");
+        if (!checkStageMemberPermission(stage, userId, familyId)) return Result.error("无权操作该阶段");
+        if ("reviewed".equals(week.getStatus())) return Result.error("已复盘不可修改计划");
+        week.setPlan(plan);
+        if ("pending".equals(week.getStatus())) week.setStatus("planned");
+        doaWeekMapper.updateById(week);
+        return Result.success();
+    }
+
+    /** 提交周复盘(B表五问) */
+    @Transactional
+    public Result<Void> submitWeekReview(Long weekId, Map<String, String> review, Long userId, Long familyId) {
+        DoaWeek week = doaWeekMapper.selectById(weekId);
+        if (week == null) return Result.error("周记录不存在");
+        DoaStage stage = doaStageMapper.selectById(week.getStageId());
+        if (stage == null || !familyId.equals(stage.getFamilyId())) return Result.error("无权操作");
+        if (!checkStageMemberPermission(stage, userId, familyId)) return Result.error("无权操作该阶段");
+        if ("reviewed".equals(week.getStatus())) return Result.error("本周已复盘,不可重复提交");
+        week.setAchieved(review.get("achieved"));
+        week.setExperience(review.get("experience"));
+        week.setWorked(review.get("worked"));
+        week.setImprove(review.get("improve"));
+        week.setNextChallenge(review.get("nextChallenge"));
+        week.setStatus("reviewed");
+        doaWeekMapper.updateById(week);
+        return Result.success();
+    }
+
+    /** 周记录列表(按阶段查,含周序号) */
+    public List<DoaWeek> listWeeks(Long stageId, Long familyId) {
+        DoaStage stage = doaStageMapper.selectById(stageId);
+        if (stage == null || !familyId.equals(stage.getFamilyId())) return new ArrayList<>();
+        LambdaQueryWrapper<DoaWeek> ww = new LambdaQueryWrapper<DoaWeek>()
+                .eq(DoaWeek::getStageId, stageId)
+                .orderByAsc(DoaWeek::getWeekNo);
+        SortUtil.applySort(ww);
+        return doaWeekMapper.selectList(ww);
+    }
+
+    /** 校验当前用户对该阶段的编辑权限(本人 或 代管不能登录成员) */
+    private boolean checkStageMemberPermission(DoaStage stage, Long userId, Long familyId) {
+        Long selfMemberId = resolveMemberIdByUserId(userId, familyId);
+        if (selfMemberId != null && selfMemberId.equals(stage.getMemberId())) return true;
+        FamilyMember target = familyMemberMapper.selectById(stage.getMemberId());
+        return target != null && target.getUserId() == null;
+    }
+
+    /** userId → 家庭内成员ID */
+    private Long resolveMemberIdByUserId(Long userId, Long familyId) {
+        if (userId == null) return null;
+        LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+                .eq(FamilyMember::getUserId, userId)
+                .eq(FamilyMember::getFamilyId, familyId);
+        SortUtil.applySort(mw);
+        List<FamilyMember> members = familyMemberMapper.selectList(mw);
+        return members.isEmpty() ? null : members.get(0).getId();
+    }
+
+    private static Date addDays(Date date, int days) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        cal.add(Calendar.DAY_OF_YEAR, days);
+        return cal.getTime();
+    }
+}