|
|
@@ -19,8 +19,11 @@ import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
+import java.util.Calendar;
|
|
|
import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@Slf4j
|
|
|
@@ -79,7 +82,7 @@ public class TaskPlanService implements TaskPlanServiceInterface {
|
|
|
Date now = new Date();
|
|
|
instance.setStartDate(now);
|
|
|
instance.setEndDate(new Date(now.getTime() + (long) pkg.getCycleDays() * 24 * 60 * 60 * 1000));
|
|
|
- instance.setStatus("active");
|
|
|
+ instance.setStatus("generated"); // 状态机: generated → reviewed → active(见统一设计)
|
|
|
instance.setTotalDays(pkg.getCycleDays());
|
|
|
instance.setCompletedDays(0);
|
|
|
instance.setCreatedAt(now);
|
|
|
@@ -118,10 +121,13 @@ public class TaskPlanService implements TaskPlanServiceInterface {
|
|
|
|
|
|
/**
|
|
|
* 方案确认:将套餐计划实例生成可执行任务(B20 补全)
|
|
|
- * 每个 plan item 生成一条 Task:memberOnly=1、sourceType=plan、sourceId=planId,
|
|
|
- * 使 ProblemCompletionService 能按 (familyId, sourceType=plan) 统计完成度发奖。
|
|
|
- * 幂等:同一计划已生成过任务则跳过。
|
|
|
+ *
|
|
|
+ * @deprecated 已被 {@link #generateTasksFromTemplate} 替代。
|
|
|
+ * 旧方法从 task_plan_items 读任务(已降级为审计快照),
|
|
|
+ * 新方法 generateTasksFromTemplate 从 task_template_items 读——两套数据源不一致,
|
|
|
+ * 若未来误调用会造成双轨。confirm 流程已改走 applyAndActivate → generateTasksFromTemplate。
|
|
|
*/
|
|
|
+ @Deprecated
|
|
|
@Transactional
|
|
|
public int generatePlanTasks(Long planId, Long familyId, Long userId) {
|
|
|
TaskPlanInstance plan = instanceMapper.selectById(planId);
|
|
|
@@ -150,7 +156,7 @@ public class TaskPlanService implements TaskPlanServiceInterface {
|
|
|
task.setFamilyId(familyId);
|
|
|
task.setChildId(plan.getChildId());
|
|
|
task.setCreatorId(userId);
|
|
|
- task.setExecutorType("child");
|
|
|
+ task.setExecutorType("child"); // 非登录用户(不可登录成员)
|
|
|
task.setExecutorId(plan.getChildId());
|
|
|
task.setTitle(item.getTaskName());
|
|
|
task.setDescription(item.getDescription());
|
|
|
@@ -179,6 +185,143 @@ public class TaskPlanService implements TaskPlanServiceInterface {
|
|
|
return count;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 统一方案任务生成器(设计: 2026-09-04-plan-to-task-unification-design.md)
|
|
|
+ * 引导任务 + 从 task_template_items(按 sort_order)批量生成日常任务。
|
|
|
+ * 幂等:sourceType=plan & sourceId=planId 已存在任务则跳过。
|
|
|
+ * deadline:当天 23:59:59;familyMemberId:统一设置;memberOnly=1。
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public int generateTasksFromTemplate(TaskPlanInstance plan, Long operatorId) {
|
|
|
+ if (plan == null) {
|
|
|
+ throw new IllegalStateException("方案不存在");
|
|
|
+ }
|
|
|
+ Long planId = plan.getId();
|
|
|
+ // 幂等:该方案已生成过任务则跳过(排除 cancelled,避免 cancelPlan 后重新激活时漏生成)
|
|
|
+ Long existing = taskMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<Task>()
|
|
|
+ .eq(Task::getSourceType, "plan")
|
|
|
+ .eq(Task::getSourceId, planId)
|
|
|
+ .ne(Task::getStatus, "cancelled"));
|
|
|
+ if (existing != null && existing > 0) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ Date now = new Date();
|
|
|
+ Date deadline = buildTodayDeadline();
|
|
|
+ int count = 0;
|
|
|
+
|
|
|
+ // 1. 引导任务(方案启动)
|
|
|
+ Task guideTask = new Task();
|
|
|
+ guideTask.setFamilyId(plan.getFamilyId());
|
|
|
+ guideTask.setChildId(plan.getChildId());
|
|
|
+ guideTask.setFamilyMemberId(plan.getChildId());
|
|
|
+ guideTask.setCreatorId(operatorId);
|
|
|
+ guideTask.setExecutorType("child"); // 非登录用户(不可登录成员)
|
|
|
+ guideTask.setExecutorId(plan.getChildId());
|
|
|
+ guideTask.setTitle("方案启动: " + plan.getName());
|
|
|
+ guideTask.setDescription("请阅读并开始执行测评方案");
|
|
|
+ guideTask.setPoints(2);
|
|
|
+ guideTask.setCategory("成长");
|
|
|
+ guideTask.setStatus("pending");
|
|
|
+ guideTask.setSourceType("plan");
|
|
|
+ guideTask.setSourceId(planId);
|
|
|
+ guideTask.setNeedReview(0);
|
|
|
+ guideTask.setIsTemplate(0);
|
|
|
+ guideTask.setMemberOnly(1);
|
|
|
+ guideTask.setDeadline(deadline);
|
|
|
+ guideTask.setCreatedAt(now);
|
|
|
+ guideTask.setUpdatedAt(now);
|
|
|
+ taskMapper.insert(guideTask);
|
|
|
+ count++;
|
|
|
+
|
|
|
+ // 2. 模板任务(task_template_items 为唯一来源)
|
|
|
+ if (plan.getPackageId() != null) {
|
|
|
+ List<TaskTemplateItem> items = templateItemMapper.selectList(
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplateItem>()
|
|
|
+ .eq("package_id", plan.getPackageId())
|
|
|
+ .orderByAsc("sort_order"));
|
|
|
+ for (TaskTemplateItem item : items) {
|
|
|
+ if (item.getTaskName() == null || item.getTaskName().isEmpty()) continue;
|
|
|
+ Task task = new Task();
|
|
|
+ task.setFamilyId(plan.getFamilyId());
|
|
|
+ task.setChildId(plan.getChildId());
|
|
|
+ task.setFamilyMemberId(plan.getChildId());
|
|
|
+ task.setCreatorId(operatorId);
|
|
|
+ task.setExecutorType("child"); // 非登录用户(不可登录成员)
|
|
|
+ task.setExecutorId(plan.getChildId());
|
|
|
+ task.setTitle(item.getTaskName());
|
|
|
+ task.setDescription(item.getDescription());
|
|
|
+ task.setPoints(item.getPoints() != null ? item.getPoints() : 2);
|
|
|
+ task.setCategory("成长");
|
|
|
+ task.setStatus("pending");
|
|
|
+ task.setSourceType("plan");
|
|
|
+ task.setSourceId(planId);
|
|
|
+ task.setNeedReview(0);
|
|
|
+ task.setIsTemplate(0);
|
|
|
+ task.setMemberOnly(1);
|
|
|
+ task.setFrequency(item.getFrequency() != null ? item.getFrequency() : "daily");
|
|
|
+ task.setDuration(item.getDuration());
|
|
|
+ task.setDeadline(deadline);
|
|
|
+ task.setCreatedAt(now);
|
|
|
+ task.setUpdatedAt(now);
|
|
|
+ taskMapper.insert(task);
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("方案统一生成任务: planId={}, familyId={}, taskCount={}", planId, plan.getFamilyId(), count);
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 家长自助应用套餐并激活(confirm / purchase 共用):
|
|
|
+ * 1. 幂等:同家庭同套餐已有 active 实例则复用
|
|
|
+ * 2. 新建则 applyPackage(generated)→ 置 active(自助跳过审核)
|
|
|
+ * 3. 统一生成器生成任务
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public Map<String, Object> applyAndActivate(ApplyPackageDTO dto, Long familyId, Long userId) {
|
|
|
+ TaskPlanInstance plan = findActiveByFamilyAndPackage(familyId, dto.getPackageId());
|
|
|
+ boolean created = false;
|
|
|
+ if (plan == null) {
|
|
|
+ plan = applyPackage(dto, familyId);
|
|
|
+ created = true;
|
|
|
+ }
|
|
|
+ if (created) {
|
|
|
+ Date now = new Date();
|
|
|
+ plan.setStatus("active");
|
|
|
+ plan.setActivatedAt(now);
|
|
|
+ plan.setUpdatedAt(now);
|
|
|
+ instanceMapper.updateById(plan);
|
|
|
+ }
|
|
|
+ int taskCount = generateTasksFromTemplate(plan, userId);
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("planId", plan.getId());
|
|
|
+ result.put("taskCount", taskCount);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询同家庭同套餐的 active 实例(幂等复用) */
|
|
|
+ public TaskPlanInstance findActiveByFamilyAndPackage(Long familyId, Long packageId) {
|
|
|
+ return instanceMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<TaskPlanInstance>()
|
|
|
+ .eq(TaskPlanInstance::getFamilyId, familyId)
|
|
|
+ .eq(TaskPlanInstance::getPackageId, packageId)
|
|
|
+ .eq(TaskPlanInstance::getStatus, "active")
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 当天 23:59:59 截止(与 TaskService.getTodayTasks 区间对齐) */
|
|
|
+ private Date buildTodayDeadline() {
|
|
|
+ Calendar cal = Calendar.getInstance();
|
|
|
+ cal.set(Calendar.HOUR_OF_DAY, 23);
|
|
|
+ cal.set(Calendar.MINUTE, 59);
|
|
|
+ cal.set(Calendar.SECOND, 59);
|
|
|
+ cal.set(Calendar.MILLISECOND, 0);
|
|
|
+ return cal.getTime();
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 获取计划详情
|
|
|
*/
|
|
|
@@ -199,6 +342,7 @@ public class TaskPlanService implements TaskPlanServiceInterface {
|
|
|
/**
|
|
|
* 取消计划
|
|
|
*/
|
|
|
+ @Transactional
|
|
|
public boolean cancelPlan(Long id) {
|
|
|
TaskPlanInstance instance = instanceMapper.selectById(id);
|
|
|
if (instance == null || !"active".equals(instance.getStatus())) {
|
|
|
@@ -206,7 +350,23 @@ public class TaskPlanService implements TaskPlanServiceInterface {
|
|
|
}
|
|
|
instance.setStatus("cancelled");
|
|
|
instance.setUpdatedAt(new Date());
|
|
|
- return instanceMapper.updateById(instance) > 0;
|
|
|
+ boolean updated = instanceMapper.updateById(instance) > 0;
|
|
|
+
|
|
|
+ // 级联清理:该方案生成的未完成任务置 cancelled
|
|
|
+ if (updated) {
|
|
|
+ List<Task> pendingTasks = taskMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<Task>()
|
|
|
+ .eq(Task::getSourceType, "plan")
|
|
|
+ .eq(Task::getSourceId, id)
|
|
|
+ .in(Task::getStatus, "pending", "pending_review", "locked", "in_progress"));
|
|
|
+ for (Task t : pendingTasks) {
|
|
|
+ t.setStatus("cancelled");
|
|
|
+ t.setUpdatedAt(new Date());
|
|
|
+ taskMapper.updateById(t);
|
|
|
+ }
|
|
|
+ log.info("取消方案{}并级联清理{}条未完成任务", id, pendingTasks.size());
|
|
|
+ }
|
|
|
+ return updated;
|
|
|
}
|
|
|
|
|
|
/**
|