|
|
@@ -197,6 +197,9 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
QueryWrapper<Task> wrapper = new QueryWrapper<>();
|
|
|
wrapper.eq("source_type", "health_plan")
|
|
|
.eq("source_id", planId)
|
|
|
+ // 直接任务语义:每日实例带 template_id(由周期模板生成),不属于方案任务列表,
|
|
|
+ // 否则列表会按天膨胀,且与"直接任务"幂等/删除口径不一致
|
|
|
+ .isNull("template_id")
|
|
|
.orderByAsc("id");
|
|
|
return taskMapper.selectList(wrapper);
|
|
|
}
|
|
|
@@ -213,13 +216,15 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- // 1. 查方案直接任务(周期模板 + 一次性任务 + 购买锚点)
|
|
|
+ // 1. 查方案直接任务(周期模板 + 一次性任务 + 购买锚点);排除按日生成的实例(template_id 非空)
|
|
|
List<Task> planTasks = taskMapper.selectList(new QueryWrapper<Task>()
|
|
|
.eq("source_type", "health_plan")
|
|
|
- .eq("source_id", planId));
|
|
|
+ .eq("source_id", planId)
|
|
|
+ .isNull("template_id"));
|
|
|
int deleted = 0;
|
|
|
|
|
|
- // 2. 先删每日实例:实例只透传 template_id、不带 source_id,漏删会留孤儿任务且次日继续生成
|
|
|
+ // 2. 先删每日实例:按 template_id 指向的直接任务精确删除(实例已透传 source_type/source_id,
|
|
|
+ // 但 template_id 维度最精准,避免误删其他来源任务)
|
|
|
if (planTasks != null && !planTasks.isEmpty()) {
|
|
|
List<Long> taskIds = new java.util.ArrayList<Long>();
|
|
|
for (Task t : planTasks) {
|
|
|
@@ -230,7 +235,8 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 3. 删直接任务
|
|
|
+ // 3. 删直接任务 + source 维度兜底:任何仍带 health_plan 来源的行(含已透传 source 的实例)一并清掉,
|
|
|
+ // 保证不因透传/模板归属差异留下孤儿任务
|
|
|
deleted += taskMapper.delete(new QueryWrapper<Task>()
|
|
|
.eq("source_type", "health_plan")
|
|
|
.eq("source_id", planId));
|
|
|
@@ -475,7 +481,9 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
*/
|
|
|
private void generateDailyTasksForMembers(HealthPlan plan, JsonNode root, String[] memberIdArray) {
|
|
|
Long familyId = plan.getFamilyId();
|
|
|
- List<TaskDraft> commonDrafts = buildDraftsFromSections(root.path("common").path("sections"));
|
|
|
+ // 懒加载回填标记:common/individual 任一 section 回填了 tasks 即整体落库
|
|
|
+ boolean[] backfilled = new boolean[]{false};
|
|
|
+ List<TaskDraft> commonDrafts = extractDraftsFromSections(root.path("common").path("sections"), backfilled);
|
|
|
Date now = new Date();
|
|
|
int totalDays = (plan.getTotalDays() != null && plan.getTotalDays() > 0)
|
|
|
? plan.getTotalDays() : DEFAULT_PLAN_DAYS;
|
|
|
@@ -486,28 +494,68 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
Long rawId = Long.valueOf(rawMemberId.trim());
|
|
|
Long familyMemberId = resolveFamilyMemberId(rawId, familyId);
|
|
|
List<TaskDraft> memberDrafts = new java.util.ArrayList<>(commonDrafts);
|
|
|
- memberDrafts.addAll(buildDraftsFromSections(
|
|
|
- root.path("individual").path(rawMemberId.trim()).path("sections")));
|
|
|
+ memberDrafts.addAll(extractDraftsFromSections(
|
|
|
+ root.path("individual").path(rawMemberId.trim()).path("sections"), backfilled));
|
|
|
if (memberDrafts.isEmpty()) continue;
|
|
|
insertTasksForMember(plan, familyId, familyMemberId, memberDrafts, now, planEnd);
|
|
|
}
|
|
|
+
|
|
|
+ // 多人路径懒回填:回填内容当次落库 plan_json,避免下次发布时再次重复提取
|
|
|
+ if (backfilled[0]) {
|
|
|
+ try {
|
|
|
+ plan.setPlanJson(objectMapper.writeValueAsString(root));
|
|
|
+ healthPlanMapper.updateById(plan);
|
|
|
+ log.info("方案{}多人懒加载回填 tasks 完成", plan.getId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("方案{}多人回填 tasks 写库失败,不影响本次任务生成: {}", plan.getId(), e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /** 从 planJson 的 sections 数组提取任务草稿(仅读取 tasks 字段,无懒回填) */
|
|
|
- private List<TaskDraft> buildDraftsFromSections(JsonNode sections) {
|
|
|
+ /** 从 planJson 的 sections 数组提取任务草稿(唯一读取/回填入口,单人与多人路径共用)。 */
|
|
|
+ private List<TaskDraft> extractDraftsFromSections(JsonNode sections, boolean[] backfilled) {
|
|
|
List<TaskDraft> drafts = new java.util.ArrayList<>();
|
|
|
if (sections == null || !sections.isArray()) return drafts;
|
|
|
for (JsonNode section : sections) {
|
|
|
JsonNode tasks = section.path("tasks");
|
|
|
- if (!tasks.isArray()) continue;
|
|
|
- for (JsonNode t : tasks) {
|
|
|
- String actionType = t.path("action_type").asText("");
|
|
|
- String title = t.path("title").asText("");
|
|
|
- String dimension = t.path("dimension").asText("");
|
|
|
- String frequency = t.path("frequency").asText("daily");
|
|
|
- if (title.isEmpty()) continue;
|
|
|
- TaskDraft draft = mapStructuredTask(actionType, title, dimension, frequency);
|
|
|
- if (draft != null) drafts.add(draft);
|
|
|
+ if (tasks.isArray() && tasks.size() > 0) {
|
|
|
+ for (JsonNode t : tasks) {
|
|
|
+ String actionType = t.path("action_type").asText("");
|
|
|
+ String title = t.path("title").asText("");
|
|
|
+ String dimension = t.path("dimension").asText("");
|
|
|
+ String frequency = t.path("frequency").asText("daily");
|
|
|
+ if (title.isEmpty()) continue;
|
|
|
+ TaskDraft draft = mapStructuredTask(actionType, title, dimension, frequency);
|
|
|
+ if (draft != null) drafts.add(draft);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 懒加载回填:tasks 缺失/为空/非法时,用正则从 content 提取动作行;
|
|
|
+ // 仅当提取结果非空才写回 section.tasks(避免空回填反复写库),backfilled[0] 置 true 供调用方落库。
|
|
|
+ List<String> lines = parseTaskLines(section.path("content").asText(""));
|
|
|
+ ArrayNode newTasks = objectMapper.createArrayNode();
|
|
|
+ boolean sectionBackfilled = false;
|
|
|
+ for (String raw : lines) {
|
|
|
+ String line = raw.trim();
|
|
|
+ if (line.isEmpty()) continue;
|
|
|
+ List<TaskDraft> cls = classifyTaskLine(line);
|
|
|
+ if (cls == null) continue;
|
|
|
+ for (TaskDraft d : cls) {
|
|
|
+ ObjectNode node = objectMapper.createObjectNode();
|
|
|
+ node.put("action_type", actionTypeFromCategory(d.categoryLabel));
|
|
|
+ node.put("title", d.title);
|
|
|
+ node.put("dimension", d.dimensionCode);
|
|
|
+ node.put("frequency", d.frequency);
|
|
|
+ node.put("notes", "");
|
|
|
+ newTasks.add(node);
|
|
|
+ drafts.add(d); // 回填内容当次即消费,不等下次发布
|
|
|
+ }
|
|
|
+ sectionBackfilled = true;
|
|
|
+ }
|
|
|
+ // 仅在有可提取任务时写回;非 ObjectNode 的 section(异常结构)跳过写回
|
|
|
+ if (sectionBackfilled && section instanceof ObjectNode) {
|
|
|
+ ((ObjectNode) section).set("tasks", newTasks);
|
|
|
+ if (backfilled != null) backfilled[0] = true;
|
|
|
}
|
|
|
}
|
|
|
return drafts;
|
|
|
@@ -644,7 +692,9 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
.eq("source_type", "health_plan")
|
|
|
.eq("source_id", plan.getId())
|
|
|
.eq("title", title)
|
|
|
- .eq("is_template", templateOnly);
|
|
|
+ .eq("is_template", templateOnly)
|
|
|
+ // 幂等口径与直接任务一致:每日实例带 template_id,不得误判为"已存在任务"而跳过生成
|
|
|
+ .isNull("template_id");
|
|
|
if (category != null) qw.eq("category", category);
|
|
|
if (familyMemberId != null) qw.eq("family_member_id", familyMemberId);
|
|
|
return taskMapper.selectOne(qw.last("LIMIT 1"));
|
|
|
@@ -671,6 +721,10 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
return task;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 单人/旧 flat 方案:从 plan_json 的 root.sections 提取任务草稿;
|
|
|
+ * 懒加载回填统一走 extractDraftsFromSections,回填内容既落库也当次返回。
|
|
|
+ */
|
|
|
private List<TaskDraft> buildDraftsFromPlanJson(HealthPlan plan) {
|
|
|
List<TaskDraft> drafts = new java.util.ArrayList<>();
|
|
|
String planJsonStr = plan.getPlanJson();
|
|
|
@@ -681,45 +735,10 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
JsonNode sections = root.path("sections");
|
|
|
if (!sections.isArray()) return drafts;
|
|
|
|
|
|
- boolean backfilled = false;
|
|
|
- for (JsonNode section : sections) {
|
|
|
- JsonNode tasks = section.path("tasks");
|
|
|
- if (tasks.isArray()) {
|
|
|
- for (JsonNode t : tasks) {
|
|
|
- String actionType = t.path("action_type").asText("");
|
|
|
- String title = t.path("title").asText("");
|
|
|
- String dimension = t.path("dimension").asText("");
|
|
|
- String frequency = t.path("frequency").asText("daily");
|
|
|
- if (title.isEmpty()) continue;
|
|
|
- TaskDraft draft = mapStructuredTask(actionType, title, dimension, frequency);
|
|
|
- if (draft != null) drafts.add(draft);
|
|
|
- }
|
|
|
- }
|
|
|
- // 懒加载回填:section 无 tasks 时用正则从 content 提取并写回
|
|
|
- if (tasks.isMissingNode() || (tasks.isArray() && tasks.size() == 0)) {
|
|
|
- String sectionContent = section.path("content").asText("");
|
|
|
- List<String> lines = parseTaskLines(sectionContent);
|
|
|
- ArrayNode newTasks = objectMapper.createArrayNode();
|
|
|
- for (String raw : lines) {
|
|
|
- String line = raw.trim();
|
|
|
- if (line.isEmpty()) continue;
|
|
|
- List<TaskDraft> cls = classifyTaskLine(line);
|
|
|
- if (cls == null) continue;
|
|
|
- for (TaskDraft d : cls) {
|
|
|
- ObjectNode node = objectMapper.createObjectNode();
|
|
|
- node.put("action_type", actionTypeFromCategory(d.categoryLabel));
|
|
|
- node.put("title", d.title);
|
|
|
- node.put("dimension", d.dimensionCode);
|
|
|
- node.put("frequency", d.frequency);
|
|
|
- node.put("notes", "");
|
|
|
- newTasks.add(node);
|
|
|
- }
|
|
|
- }
|
|
|
- ((ObjectNode) section).set("tasks", newTasks);
|
|
|
- backfilled = true;
|
|
|
- }
|
|
|
- }
|
|
|
- if (backfilled) {
|
|
|
+ boolean[] backfilled = new boolean[]{false};
|
|
|
+ drafts.addAll(extractDraftsFromSections(sections, backfilled));
|
|
|
+
|
|
|
+ if (backfilled[0]) {
|
|
|
plan.setPlanJson(objectMapper.writeValueAsString(root));
|
|
|
healthPlanMapper.updateById(plan);
|
|
|
log.info("方案{}懒加载回填 tasks 完成", plan.getId());
|
|
|
@@ -827,9 +846,11 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
int taskCount = 0;
|
|
|
try {
|
|
|
generateDailyTasksFromPlan(plan);
|
|
|
+ // 任务计数只统计直接任务(周期模板 + 一次性任务),排除按日生成的实例
|
|
|
List<Task> generated = taskMapper.selectList(new QueryWrapper<Task>()
|
|
|
.eq("source_type", "health_plan")
|
|
|
- .eq("source_id", planId));
|
|
|
+ .eq("source_id", planId)
|
|
|
+ .isNull("template_id"));
|
|
|
taskCount = generated == null ? 0 : generated.size();
|
|
|
taskGenerated = taskCount > 0;
|
|
|
} catch (Exception e) {
|