|
|
@@ -217,6 +217,12 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
|
|
|
@Override
|
|
|
public String generatePlan(Long familyId, String memberIds, String dimensions, String goal) {
|
|
|
+ Object result = generatePlanResult(familyId, memberIds, dimensions, goal);
|
|
|
+ return (result instanceof String) ? (String) result : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Object generatePlanResult(Long familyId, String memberIds, String dimensions, String goal) {
|
|
|
if (langgraphBaseUrl == null || langgraphBaseUrl.trim().isEmpty()) {
|
|
|
return buildSamplePlan(memberIds, dimensions, goal);
|
|
|
}
|
|
|
@@ -233,6 +239,15 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
JsonNode root = objectMapper.readTree(response.getBody());
|
|
|
if (root.has("success") && root.get("success").asBoolean()) {
|
|
|
JsonNode data = root.get("data");
|
|
|
+ // 多人方案:common(共性)+ individual(各成员个性)
|
|
|
+ if (data != null && data.has("common")) {
|
|
|
+ Map<String, Object> multi = new HashMap<>();
|
|
|
+ multi.put("planContent", buildMultiMemberPlanContent(data));
|
|
|
+ if (data.has("common")) multi.put("common", data.get("common"));
|
|
|
+ if (data.has("individual")) multi.put("individual", data.get("individual"));
|
|
|
+ return multi;
|
|
|
+ }
|
|
|
+ // 单人生成:保持原 flat 结构
|
|
|
if (data != null && data.has("overview")) {
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
sb.append("## 方案概述\n\n").append(data.get("overview").asText()).append("\n\n");
|
|
|
@@ -254,6 +269,45 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
return buildSamplePlan(memberIds, dimensions, goal);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 渲染多人方案 planContent:先共性,再各成员个性。
|
|
|
+ * LangGraph 返回结构:{common: {overview, sections}, individual: {memberId: {overview, sections}}}
|
|
|
+ */
|
|
|
+ private String buildMultiMemberPlanContent(JsonNode data) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ JsonNode common = data.get("common");
|
|
|
+ if (common != null && common.has("overview")) {
|
|
|
+ sb.append("# 共性方案\n\n").append(common.get("overview").asText()).append("\n\n");
|
|
|
+ }
|
|
|
+ if (common != null && common.has("sections")) {
|
|
|
+ for (JsonNode section : common.get("sections")) {
|
|
|
+ sb.append("## ").append(section.get("title").asText()).append("\n\n");
|
|
|
+ sb.append(section.get("content").asText()).append("\n\n");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JsonNode individual = data.get("individual");
|
|
|
+ if (individual != null && individual.isObject()) {
|
|
|
+ java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = individual.fields();
|
|
|
+ while (fields.hasNext()) {
|
|
|
+ java.util.Map.Entry<String, JsonNode> entry = fields.next();
|
|
|
+ String memberId = entry.getKey();
|
|
|
+ JsonNode part = entry.getValue();
|
|
|
+ if (part == null) continue;
|
|
|
+ sb.append("\n---\n\n# 成员 ").append(memberId).append(" 个性方案\n\n");
|
|
|
+ if (part.has("overview")) {
|
|
|
+ sb.append("## 概述\n\n").append(part.get("overview").asText()).append("\n\n");
|
|
|
+ }
|
|
|
+ if (part.has("sections")) {
|
|
|
+ for (JsonNode section : part.get("sections")) {
|
|
|
+ sb.append("## ").append(section.get("title").asText()).append("\n\n");
|
|
|
+ sb.append(section.get("content").asText()).append("\n\n");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
@Override
|
|
|
public String regenerateSection(Long familyId, String section, String feedback, String planJsonStr) {
|
|
|
@@ -318,6 +372,20 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
private static final int DEFAULT_PLAN_DAYS = 14;
|
|
|
|
|
|
private void generateDailyTasksFromPlan(HealthPlan plan) {
|
|
|
+ Long familyId = plan.getFamilyId();
|
|
|
+ String[] memberIdArray = (plan.getMemberIds() != null && !plan.getMemberIds().isEmpty())
|
|
|
+ ? plan.getMemberIds().split(",") : new String[0];
|
|
|
+
|
|
|
+ // 多人结构:planJson 含 common + individual,且 memberIds 多于 1 人
|
|
|
+ // 规则:共性任务给每个成员各生成一份;个性任务只给对应成员
|
|
|
+ JsonNode planRoot = parsePlanJson(plan.getPlanJson());
|
|
|
+ if (planRoot != null && planRoot.has("common") && planRoot.has("individual")
|
|
|
+ && memberIdArray.length > 1) {
|
|
|
+ generateDailyTasksForMembers(plan, planRoot, memberIdArray);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 单人生成 / 旧 flat 结构:保持原有逻辑(只取第一个成员)
|
|
|
String content = plan.getPlanContent();
|
|
|
List<String> rawLines = new java.util.ArrayList<>();
|
|
|
List<TaskDraft> structuredDrafts = buildDraftsFromPlanJson(plan);
|
|
|
@@ -331,18 +399,12 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- Long familyId = plan.getFamilyId();
|
|
|
- String[] memberIdArray = (plan.getMemberIds() != null && !plan.getMemberIds().isEmpty())
|
|
|
- ? plan.getMemberIds().split(",") : new String[0];
|
|
|
Long rawMemberId = memberIdArray.length > 0 ? Long.valueOf(memberIdArray[0].trim()) : null;
|
|
|
Long familyMemberId = resolveFamilyMemberId(rawMemberId, familyId);
|
|
|
- Long childId = familyMemberId;
|
|
|
- Long executorId = familyMemberId;
|
|
|
|
|
|
Date now = new Date();
|
|
|
|
|
|
// 方案周期终点(D3):由 health_plans.total_days 决定,缺省 14 天。
|
|
|
- // 既作为周期任务模板的截止日期(其时分=每日截止时刻),也作为周期生成的终止边界。
|
|
|
int totalDays = (plan.getTotalDays() != null && plan.getTotalDays() > 0)
|
|
|
? plan.getTotalDays() : DEFAULT_PLAN_DAYS;
|
|
|
Date planEnd = buildPlanEnd(now, totalDays);
|
|
|
@@ -359,9 +421,70 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ insertTasksForMember(plan, familyId, familyMemberId, drafts, now, planEnd);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 多人方案:按成员分别生成任务。
|
|
|
+ * 共性任务(common.sections.tasks)每个成员各一份;个性任务(individual.<memberId>.sections.tasks)只给对应成员。
|
|
|
+ */
|
|
|
+ private void generateDailyTasksForMembers(HealthPlan plan, JsonNode root, String[] memberIdArray) {
|
|
|
+ Long familyId = plan.getFamilyId();
|
|
|
+ List<TaskDraft> commonDrafts = buildDraftsFromSections(root.path("common").path("sections"));
|
|
|
+ Date now = new Date();
|
|
|
+ int totalDays = (plan.getTotalDays() != null && plan.getTotalDays() > 0)
|
|
|
+ ? plan.getTotalDays() : DEFAULT_PLAN_DAYS;
|
|
|
+ Date planEnd = buildPlanEnd(now, totalDays);
|
|
|
+
|
|
|
+ for (String rawMemberId : memberIdArray) {
|
|
|
+ if (rawMemberId == null || rawMemberId.trim().isEmpty()) continue;
|
|
|
+ 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")));
|
|
|
+ if (memberDrafts.isEmpty()) continue;
|
|
|
+ insertTasksForMember(plan, familyId, familyMemberId, memberDrafts, now, planEnd);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从 planJson 的 sections 数组提取任务草稿(仅读取 tasks 字段,无懒回填) */
|
|
|
+ private List<TaskDraft> buildDraftsFromSections(JsonNode sections) {
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return drafts;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JsonNode parsePlanJson(String planJsonStr) {
|
|
|
+ if (planJsonStr == null || planJsonStr.trim().isEmpty()) return null;
|
|
|
+ try {
|
|
|
+ return objectMapper.readTree(planJsonStr);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析 plan_json 失败: {}", e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 为指定成员插入任务:一次性购买锚点 + 一次性/周期任务模板 */
|
|
|
+ private void insertTasksForMember(HealthPlan plan, Long familyId, Long familyMemberId,
|
|
|
+ List<TaskDraft> drafts, Date now, Date planEnd) {
|
|
|
+ Long childId = familyMemberId;
|
|
|
+ Long executorId = familyMemberId;
|
|
|
+
|
|
|
// 1) 先落一次性购买任务,作为服务任务的依赖锚点(D1)
|
|
|
- // 依赖必须"逐项配对":每个使用任务绑定它前面最近的购买任务。
|
|
|
- // 若所有使用任务共用首个采购任务,多商品场景闸门语义就是错的(E2E 实跑发现)。
|
|
|
Long buyTaskId = null;
|
|
|
List<Long> draftAnchors = new java.util.ArrayList<Long>();
|
|
|
for (TaskDraft draft : drafts) {
|
|
|
@@ -370,7 +493,7 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
draftAnchors.add(buyTaskId);
|
|
|
continue;
|
|
|
}
|
|
|
- Task existing = findPlanTask(plan, familyId, draft.title, "购买任务", 0);
|
|
|
+ Task existing = findPlanTask(plan, familyId, familyMemberId, draft.title, "购买任务", 0);
|
|
|
if (existing != null) {
|
|
|
buyTaskId = existing.getId();
|
|
|
draftAnchors.add(existing.getId());
|
|
|
@@ -399,7 +522,7 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
boolean once = "once".equals(draft.frequency);
|
|
|
|
|
|
if (once) {
|
|
|
- if (findPlanTask(plan, familyId, draft.title, draft.categoryLabel, 0) != null) continue;
|
|
|
+ if (findPlanTask(plan, familyId, familyMemberId, draft.title, draft.categoryLabel, 0) != null) continue;
|
|
|
Task task = newPlanTask(plan, familyId, familyMemberId, childId, executorId, draft, now);
|
|
|
task.setRepeatType("none");
|
|
|
task.setTaskType("onetime");
|
|
|
@@ -415,9 +538,7 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
}
|
|
|
|
|
|
// 周期任务 -> 模板行(is_template=1),交 RepeatTaskGenerator 按日生成实例。
|
|
|
- // 模板 deadline=planEnd(未来日期),因此不会出现在"今日任务"列表里(列表按 deadline=当天过滤),
|
|
|
- // 用户只会看到生成出来的实例,天然避免模板/实例重复展示。
|
|
|
- Task existingTpl = findPlanTask(plan, familyId, draft.title, draft.categoryLabel, 1);
|
|
|
+ Task existingTpl = findPlanTask(plan, familyId, familyMemberId, draft.title, draft.categoryLabel, 1);
|
|
|
if (existingTpl != null) {
|
|
|
// 模板已存在:仅在依赖缺失时补写(修复存量方案已生成的模板)
|
|
|
if ("使用任务".equals(draft.categoryLabel)
|
|
|
@@ -467,6 +588,12 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
|
|
|
/** 幂等查找:同一方案下的同名任务,templateOnly=true 只找模板行 */
|
|
|
private Task findPlanTask(HealthPlan plan, Long familyId, String title, String category, int templateOnly) {
|
|
|
+ return findPlanTask(plan, familyId, null, title, category, templateOnly);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 按成员隔离的幂等查找:multi-member 下共性任务每成员各一份,需按 family_member_id 区分,避免互相拦截 */
|
|
|
+ private Task findPlanTask(HealthPlan plan, Long familyId, Long familyMemberId,
|
|
|
+ String title, String category, int templateOnly) {
|
|
|
QueryWrapper<Task> qw = new QueryWrapper<Task>()
|
|
|
.eq("family_id", familyId)
|
|
|
.eq("source_type", "health_plan")
|
|
|
@@ -474,6 +601,7 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
.eq("title", title)
|
|
|
.eq("is_template", templateOnly);
|
|
|
if (category != null) qw.eq("category", category);
|
|
|
+ if (familyMemberId != null) qw.eq("family_member_id", familyMemberId);
|
|
|
return taskMapper.selectOne(qw.last("LIMIT 1"));
|
|
|
}
|
|
|
|
|
|
@@ -1006,6 +1134,16 @@ public class HealthPlanServiceImpl implements HealthPlanService {
|
|
|
|
|
|
private String buildPlanJsonFromData(JsonNode data) {
|
|
|
try {
|
|
|
+ // 多人方案:原样透传 common + individual 结构(与前端保存的 planJson 一致),
|
|
|
+ // 供确认时 generateDailyTasksForMembers 按成员分别生成任务
|
|
|
+ if (data.has("common") && data.has("individual")) {
|
|
|
+ Map<String, Object> planJsonMap = new HashMap<>();
|
|
|
+ planJsonMap.put("common", objectMapper.convertValue(data.get("common"), Map.class));
|
|
|
+ planJsonMap.put("individual", objectMapper.convertValue(data.get("individual"), Map.class));
|
|
|
+ return objectMapper.writeValueAsString(planJsonMap);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 单人 / 旧 flat 结构
|
|
|
Map<String, Object> planJsonMap = new HashMap<>();
|
|
|
planJsonMap.put("overview", data.has("overview") ? data.get("overview").asText() : "");
|
|
|
List<Map<String, Object>> sections = new ArrayList<>();
|