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