Explorar o código

feat(kb): P2 补剂相互作用库 + 干预方案结构化 + 校验器升级

- interactions.json: 10 条相互作用(铁剂×维C/姜黄/钙/茶/咖啡,益生菌×抗生素,
  Akk×铁剂,色氨酸×SSRI,鱼油×抗凝药,姜黄×抗血小板,益生菌×高温)
- protocols/gut_joint_intervention_v1.json: 双人肠道菌群联合干预方案
  完整结构化(周期定义、双成员、每阶段 5 时段补剂引用、每日/每周监测、
  4/8/12 周复查节点、运动引用);已部署的 45 条任务数据从此文件派生
- schema/ 新增 interaction.schema.json + protocol.schema.json
- validate_kb.py 升级:支持 interactions.json 校验 + 跨库引用完整性检查
  (supplement_ids / exercise_ids / indicator_ids / cycle_id 交叉验证)
Xiaogang Liao hai 1 semana
pai
achega
a421284569

+ 191 - 64
docs/scripts/validate_kb.py

@@ -1,10 +1,17 @@
 #!/usr/bin/env python3
-"""知识库质量校验:JSON Schema + 引用完整性。
+"""知识库质量校验:JSON Schema 结构 + 跨库引用完整性。
 
 用法:
     python3 validate_kb.py [知识库目录]
-默认校验 docs/参考资料/知识库/ 下新增的 3 个子库及其 schema。
-无 jsonschema 依赖时降级为内置结构校验。
+
+校验清单:
+  1. 各子库 JSON 可解析、顶层字段存在;
+  2. id 唯一、id 前缀模式正确(sup-/ind-/ex-/ix-/protocol-);
+  3. 枚举值合法(category/timing/form/type/intensity/severity 等);
+  4. 跨库引用完整性:
+     - protocols: supplement_ids->supplements.json, exercise_ids->exercises.json,
+       indicator_ids->clinical_indicators.json, cycle_id->cycles
+     - interactions: supplement_id->supplements.json
 """
 import json
 import re
@@ -17,12 +24,17 @@ SCHEMA_FILES = {
     "supplements.json": "schema/supplement.schema.json",
     "clinical_indicators.json": "schema/clinical_indicator.schema.json",
     "exercises.json": "schema/exercise.schema.json",
+    "interactions.json": "schema/interaction.schema.json",
 }
 
+PROTOCOL_SCHEMA = "schema/protocol.schema.json"
+
 ID_PATTERNS = {
     "supplements.json": ("supplements", "id", re.compile(r"^sup-[a-z0-9-]+$")),
     "clinical_indicators.json": ("indicators", "id", re.compile(r"^ind-[a-z0-9-]+$")),
     "exercises.json": ("exercises", "id", re.compile(r"^ex-[a-z0-9-]+$")),
+    "interactions.json": ("interactions", "id", re.compile(r"^ix-[a-z0-9-]+$")),
+    "protocols": ("members/phases", "protocol", re.compile(r"^protocol-[a-z0-9-]+$")),
 }
 
 CATEGORY_ENUM = {"益生菌", "益生元", "氨基酸", "维生素", "矿物质", "脂肪酸", "植物提取物"}
@@ -30,6 +42,8 @@ TIMING_ENUM = {"晨起空腹", "早餐后", "午餐后", "晚餐后", "睡前",
 FORM_ENUM = {"粉剂", "胶囊", "片剂", "滴剂", "颗粒", "液体"}
 EX_TYPE_ENUM = {"有氧", "抗阻", "柔韧", "平衡", "其他"}
 EX_INTENSITY_ENUM = {"低", "中低", "中", "中高", "高"}
+IX_TYPE_ENUM = {"协同", "拮抗", "时序禁忌", "安全警示"}
+IX_SEVERITY_ENUM = {"提示", "警示", "禁忌"}
 
 
 def load(path: Path):
@@ -39,94 +53,207 @@ def load(path: Path):
         raise SystemExit(f"JSON 解析失败: {path} -> {e}")
 
 
-def class_check(field, value, allowed, errors, where):
+def check_enum(field, value, allowed, errors, where):
     if value not in allowed:
         errors.append(f"{where}.{field}={value!r} 不在枚举 {sorted(allowed)} 内")
 
 
-def validate_doc(path: Path, schema_path, kind):
-    errors = []
-    doc = {"doc": load(path), "schema": load(schema_path)}
-    root = doc["doc"]
-    items = root.get(kind)
-    if items is None:
-        raise SystemExit(f"{path} 缺少顶层数组字段 {kind}")
-
+def validate_list_ids(path: Path, items, item_key, id_pattern, errors):
     seen = set()
     for i, item in enumerate(items):
         where = f"{path.name}[{i}]"
         if not isinstance(item, dict):
             errors.append(f"{where} 不是对象")
             continue
-        id_key = ID_PATTERNS[path.name][1]
-        if id_key not in item:
-            errors.append(f"{where} 缺 id")
-        else:
-            iid = item[id_key]
-            if iid in seen:
-                errors.append(f"{where} 重复 id: {iid}")
-            seen.add(iid)
-            pat = ID_PATTERNS[path.name][2]
-            if not pat.match(iid):
-                errors.append(f"{where} id={iid!r} 不匹配 {pat.pattern}")
-
-    if path.name == "supplements.json":
-        for i, item in enumerate(items):
-            where = f"{path.name}[{i}]"
-            for req in ("name", "category", "key_ingredients", "form", "dose_reference", "timing"):
-                if req not in item:
-                    errors.append(f"{where} 缺必需字段 {req}")
-            class_check("category", item.get("category"), CATEGORY_ENUM, errors, where)
-            class_check("timing", item.get("timing"), TIMING_ENUM, errors, where)
-            class_check("form", item.get("form"), FORM_ENUM, errors, where)
-            for ing in item.get("key_ingredients", []):
-                if ing.get("name") is None or ing.get("amount") is None:
-                    errors.append(f"{where}.key_ingredients 缺 name/amount")
-    elif path.name == "clinical_indicators.json":
-        for i, item in enumerate(items):
-            where = f"{path.name}[{i}]"
-            for req in ("name", "unit", "normal_range"):
-                if req not in item:
-                    errors.append(f"{where} 缺必需字段 {req}")
-            nr = item.get("normal_range", {})
-            if not isinstance(nr, dict) or "low" not in nr or "high" not in nr:
-                errors.append(f"{where}.normal_range 需含 low/high")
-    elif path.name == "exercises.json":
-        for i, item in enumerate(items):
-            where = f"{path.name}[{i}]"
-            for req in ("name", "type", "intensity", "duration_min", "frequency"):
-                if req not in item:
-                    errors.append(f"{where} 缺必需字段 {req}")
-            class_check("type", item.get("type"), EX_TYPE_ENUM, errors, where)
-            class_check("intensity", item.get("intensity"), EX_INTENSITY_ENUM, errors, where)
-            if not isinstance(item.get("duration_min"), int) or item.get("duration_min") < 1:
-                errors.append(f"{where}.duration_min 需为正整数")
-
-    return errors
+        iid = item.get(item_key)
+        if not iid:
+            errors.append(f"{where} 缺 {item_key}")
+            continue
+        if iid in seen:
+            errors.append(f"{where} 重复 {item_key}: {iid}")
+        seen.add(iid)
+        if not id_pattern.match(iid):
+            errors.append(f"{where} {item_key}={iid!r} 不匹配 {id_pattern.pattern}")
+
+
+def validate_supplements(path, items, errors):
+    for i, item in enumerate(items):
+        where = f"{path.name}[{i}]"
+        for req in ("name", "category", "key_ingredients", "form", "dose_reference", "timing"):
+            if req not in item:
+                errors.append(f"{where} 缺必需字段 {req}")
+        check_enum("category", item.get("category"), CATEGORY_ENUM, errors, where)
+        check_enum("timing", item.get("timing"), TIMING_ENUM, errors, where)
+        check_enum("form", item.get("form"), FORM_ENUM, errors, where)
+        for ing in item.get("key_ingredients", []):
+            if not isinstance(ing, dict) or ing.get("name") is None or ing.get("amount") is None:
+                errors.append(f"{where}.key_ingredients 缺 name/amount")
+
+
+def validate_clinical(path, items, errors):
+    for i, item in enumerate(items):
+        where = f"{path.name}[{i}]"
+        for req in ("name", "unit", "normal_range"):
+            if req not in item:
+                errors.append(f"{where} 缺必需字段 {req}")
+        nr = item.get("normal_range")
+        if not isinstance(nr, dict) or "low" not in nr or "high" not in nr:
+            errors.append(f"{where}.normal_range 需含 low/high")
+
+
+def validate_exercises(path, items, errors):
+    for i, item in enumerate(items):
+        where = f"{path.name}[{i}]"
+        for req in ("name", "type", "intensity", "duration_min", "frequency"):
+            if req not in item:
+                errors.append(f"{where} 缺必需字段 {req}")
+        check_enum("type", item.get("type"), EX_TYPE_ENUM, errors, where)
+        check_enum("intensity", item.get("intensity"), EX_INTENSITY_ENUM, errors, where)
+        if not isinstance(item.get("duration_min"), int) or item.get("duration_min") < 1:
+            errors.append(f"{where}.duration_min 需为正整数")
+
+
+def validate_interactions(path, items, errors):
+    for i, item in enumerate(items):
+        where = f"{path.name}[{i}]"
+        for req in ("type", "subjects_a", "subjects_b", "rule", "detail", "severity"):
+            if req not in item:
+                errors.append(f"{where} 缺必需字段 {req}")
+        check_enum("type", item.get("type"), IX_TYPE_ENUM, errors, where)
+        check_enum("severity", item.get("severity"), IX_SEVERITY_ENUM, errors, where)
+        for part in ("subjects_a", "subjects_b"):
+            s = item.get(part)
+            if not isinstance(s, dict):
+                errors.append(f"{where}.{part} 需为对象")
+                continue
+            if "supplement_id" not in s and "substance" not in s:
+                errors.append(f"{where}.{part} 需含 supplement_id 或 substance")
+
+
+def validate_protocol(path, doc, errors, refs):
+    root = doc
+    cycles = dict((c["id"], c) for c in root.get("cycles", []) if isinstance(c, dict) and c.get("id"))
+    for i, member in enumerate(root.get("members", [])):
+        where = f"{path.name} 成员[{i}]"
+        for req in ("family_id", "name", "user_id", "family_member_id", "phone", "phases",
+                    "daily_monitoring", "weekly_monitoring", "followups", "exercises"):
+            if req not in member:
+                errors.append(f"{where} 缺必需字段 {req}")
+        for phase in member.get("phases", []):
+            ph = f"{where}.phases"
+            if not isinstance(phase, dict) or "cycle_id" not in phase or "time_slots" not in phase:
+                errors.append(f"{ph} 缺 cycle_id/time_slots")
+                continue
+            if phase["cycle_id"] not in cycles:
+                errors.append(f"{ph}.cycle_id={phase['cycle_id']!r} 未在 cycles 中定义")
+            for slot in phase.get("time_slots", []):
+                sl = f"{ph}.time_slots"
+                if not isinstance(slot, dict) or "supplement_ids" not in slot:
+                    errors.append(f"{sl} 缺 supplement_ids")
+                    continue
+                for sid in slot["supplement_ids"]:
+                    if sid not in refs["supplements"]:
+                        errors.append(f"{sl} 引用不存在的补充剂: {sid}")
+        for wm in member.get("weekly_monitoring", []):
+            for iid in wm.get("indicator_ids", []):
+                if iid not in refs["indicators"]:
+                    errors.append(f"{where}.weekly_monitoring 引用不存在的指标: {iid}")
+        for fu in member.get("followups", []):
+            for iid in fu.get("indicator_ids", []):
+                if iid not in refs["indicators"]:
+                    errors.append(f"{where}.followups 引用不存在的指标: {iid}")
+        for eid in member.get("exercises", []):
+            if eid not in refs["exercises"]:
+                errors.append(f"{where}.exercises 引用不存在的运动: {eid}")
+
+
+def validate_interaction_refs(path, items, errors, refs):
+    for i, item in enumerate(items):
+        for part in ("subjects_a", "subjects_b"):
+            s = item.get(part) or {}
+            sid = s.get("supplement_id")
+            if sid and sid not in refs["supplements"]:
+                errors.append(f"{path.name}[{i}].{part} 引用不存在的补充剂: {sid}")
+
+
+def build_refs():
+    refs = {"supplements": set(), "indicators": set(), "exercises": set()}
+    for data_name, kind_id in (("supplements.json", 0), ("clinical_indicators.json", 0), ("exercises.json", 0)):
+        p = KB_DIR / data_name
+        if not p.exists():
+            continue
+        doc = load(p)
+        kind = ID_PATTERNS[data_name][0]
+        for item in doc.get(kind, []):
+            iid = item.get("id")
+            if iid:
+                refs["supplements" if data_name == "supplements.json" else "indicators" if data_name == "clinical_indicators.json" else "exercises"].add(iid)
+    return refs
 
 
 def main():
     failed = False
+    refs = build_refs()
+
     for data_name, schema_rel in SCHEMA_FILES.items():
         data_path = KB_DIR / data_name
-        schema_path = KB_DIR / schema_rel
         if not data_path.exists():
             print(f"[SKIP] {data_path} 不存在")
-            failed = True
             continue
-        if not schema_path.exists():
-            print(f"[FAIL] schema 缺失: {schema_path}")
+        if not (KB_DIR / schema_rel).exists():
+            print(f"[FAIL] schema 缺失: {schema_rel}")
             failed = True
             continue
+        doc = load(data_path)
         kind = ID_PATTERNS[data_name][0]
-        errors = validate_doc(data_path, schema_path, kind)
+        items = doc.get(kind)
+        if items is None:
+            print(f"[FAIL] {data_name} 缺少顶层数组字段 {kind}")
+            failed = True
+            continue
+        errors = []
+        validate_list_ids(data_path, items, "id", ID_PATTERNS[data_name][2], errors)
+        if data_name == "supplements.json":
+            validate_supplements(data_path, items, errors)
+        elif data_name == "clinical_indicators.json":
+            validate_clinical(data_path, items, errors)
+        elif data_name == "exercises.json":
+            validate_exercises(data_path, items, errors)
+        elif data_name == "interactions.json":
+            validate_interactions(data_path, items, errors)
+            validate_interaction_refs(data_path, items, errors, refs)
         if errors:
             failed = True
             print(f"[FAIL] {data_name}: {len(errors)} 个问题")
-            for e in errors[:15]:
+            for e in errors[:20]:
+                print("   -", e)
+        else:
+            print(f"[ OK ] {data_name}")
+
+    for proto in sorted((KB_DIR / "protocols").glob("*.json")):
+        if not (KB_DIR / PROTOCOL_SCHEMA).exists():
+            print(f"[FAIL] schema 缺失: {PROTOCOL_SCHEMA}")
+            failed = True
+            break
+        doc = load(proto)
+        if doc.get("id") is None:
+            print(f"[FAIL] {proto.name} 缺 id")
+            failed = True
+            continue
+        pat = ID_PATTERNS["protocols"][2]
+        if not pat.match(doc["id"]):
+            print(f"[FAIL] {proto.name} id={doc['id']!r} 不匹配 {pat.pattern}")
+            failed = True
+            continue
+        errors = []
+        validate_protocol(proto, doc, errors, refs)
+        if errors:
+            failed = True
+            print(f"[FAIL] {proto.name}: {len(errors)} 个问题")
+            for e in errors[:25]:
                 print("   -", e)
         else:
-            print(f"[ OK ] {data_name} 校验通过")
+            print(f"[ OK ] {proto.name}")
 
     print("RESULT:", "FAIL" if failed else "PASS")
     sys.exit(1 if failed else 0)

+ 106 - 0
docs/参考资料/知识库/interactions.json

@@ -0,0 +1,106 @@
+{
+  "version": "1.0",
+  "updated": "2026-10-13",
+  "interactions": [
+    {
+      "id": "ix-iron-vitc",
+      "type": "协同",
+      "subjects_a": { "supplement_id": "sup-lifeineng-iron" },
+      "subjects_b": { "supplement_id": "sup-vitamin-c" },
+      "rule": "同服促吸收",
+      "detail": "维生素C可将三价铁还原为二价铁,提高铁吸收率;铁剂配100mg维C同服",
+      "severity": "提示",
+      "source": "双人肠道菌群联合干预方案"
+    },
+    {
+      "id": "ix-iron-turmeric",
+      "type": "拮抗",
+      "subjects_a": { "supplement_id": "sup-lifeineng-iron" },
+      "subjects_b": { "supplement_id": "sup-turmeric" },
+      "rule": "错开2小时以上",
+      "detail": "姜黄(素)影响铁吸收;本方案姜黄暂不建议,先补铁",
+      "severity": "提示",
+      "source": "双人肠道菌群联合干预方案"
+    },
+    {
+      "id": "ix-iron-calcium",
+      "type": "拮抗",
+      "subjects_a": { "supplement_id": "sup-lifeineng-iron" },
+      "subjects_b": { "substance": "钙片/高钙食物" },
+      "rule": "错开2小时以上",
+      "detail": "钙与铁竞争吸收通道,同服降低铁吸收率",
+      "severity": "提示",
+      "source": "双人肠道菌群联合干预方案"
+    },
+    {
+      "id": "ix-iron-tea-coffee",
+      "type": "拮抗",
+      "subjects_a": { "supplement_id": "sup-lifeineng-iron" },
+      "subjects_b": { "substance": "茶/咖啡(鞣酸/多酚)" },
+      "rule": "错开2小时以上",
+      "detail": "鞣酸/多酚与铁形成不溶性络合物,显著抑制铁吸收",
+      "severity": "提示",
+      "source": "双人肠道菌群联合干预方案"
+    },
+    {
+      "id": "ix-probiotic-antibiotic",
+      "type": "时序禁忌",
+      "subjects_a": { "supplement_id": "sup-lelichang50" },
+      "subjects_b": { "substance": "抗生素" },
+      "rule": "间隔2小时以上",
+      "detail": "抗生素杀灭益生菌;与所有益生菌制剂间隔至少2小时",
+      "severity": "警示",
+      "source": "双人肠道菌群联合干预方案"
+    },
+    {
+      "id": "ix-akk-iron",
+      "type": "时序禁忌",
+      "subjects_a": { "supplement_id": "sup-akkermansia" },
+      "subjects_b": { "supplement_id": "sup-lifeineng-iron" },
+      "rule": "错开2小时",
+      "detail": "Akk菌与铁剂错开2小时,避免互相影响吸收",
+      "severity": "提示",
+      "source": "双人肠道菌群联合干预方案"
+    },
+    {
+      "id": "ix-tryptophan-ssri",
+      "type": "安全警示",
+      "subjects_a": { "supplement_id": "sup-tryptophan" },
+      "subjects_b": { "substance": "SSRI/SNRI/MAOI 类抗抑郁药" },
+      "rule": "咨询医生后使用",
+      "detail": "色氨酸为5-HT前体,与5-HT再摄取抑制类药物合用存在5-羟色胺综合征风险",
+      "severity": "禁忌",
+      "source": "临床用药共识"
+    },
+    {
+      "id": "ix-fishoil-anticoagulant",
+      "type": "安全警示",
+      "subjects_a": { "supplement_id": "sup-fish-oil" },
+      "subjects_b": { "substance": "抗凝药(华法林等)" },
+      "rule": "咨询医生后使用",
+      "detail": "高剂量EPA/DHA可增强抗凝效应,增加出血风险;监测凝血功能",
+      "severity": "警示",
+      "source": "临床用药共识"
+    },
+    {
+      "id": "ix-turmeric-anticoagulant",
+      "type": "安全警示",
+      "subjects_a": { "supplement_id": "sup-turmeric" },
+      "subjects_b": { "substance": "抗凝/抗血小板药(阿司匹林/华法林等)" },
+      "rule": "咨询医生后使用",
+      "detail": "姜黄素具抗血小板作用,与抗凝药合用增加出血风险",
+      "severity": "警示",
+      "source": "临床用药共识"
+    },
+    {
+      "id": "ix-probiotic-high-temp",
+      "type": "时序禁忌",
+      "subjects_a": { "supplement_id": "sup-lelichang50" },
+      "subjects_b": { "substance": "高温(≥40℃水/热饮)" },
+      "rule": "温水(≤40℃)冲服",
+      "detail": "高温灭活活菌;冲服水温不超过40℃,避免与热食同服",
+      "severity": "提示",
+      "source": "双人肠道菌群联合干预方案"
+    }
+  ]
+}

+ 104 - 0
docs/参考资料/知识库/protocols/gut_joint_intervention_v1.json

@@ -0,0 +1,104 @@
+{
+  "version": "1.0",
+  "updated": "2026-10-13",
+  "id": "protocol-gut-joint-intervention-v1",
+  "name": "双人肠道菌群联合干预方案",
+  "source_doc": "docs/参考资料/方案/双人肠道菌群联合干预方案.md (1).docx",
+  "cycles": [
+    { "id": "cycle-2", "name": "第二阶段", "start": "2026-10-13", "end": "2026-11-09" },
+    { "id": "cycle-3", "name": "第三阶段", "start": "2026-11-10", "end": "2026-12-07" }
+  ],
+  "members": [
+    {
+      "family_id": 87,
+      "name": "王卫东",
+      "user_id": 76,
+      "family_member_id": 38,
+      "phone": "15604130166",
+      "phases": [
+        {
+          "cycle_id": "cycle-2",
+          "time_slots": [
+            { "time": "06:00", "label": "晨起空腹", "supplement_ids": ["sup-lelichang50"], "dose": "乐力畅50+益生菌(无糖)1袋", "notes": "温水冲服;0蔗糖0脂肪,重建菌群主力" },
+            { "time": "07:00", "label": "早餐后", "supplement_ids": ["sup-lifeineng-iron", "sup-vitamin-c", "sup-lelichang50", "sup-turmeric", "sup-akkermansia"], "dose": "铁剂+维C / 乐力畅50+1袋 / 姜黄1-2粒 / Akk菌半粒~1粒", "notes": "力蜚能铁与姜黄错开2h;Akk从半粒起步防胀气;监测血糖" },
+            { "time": "12:30", "label": "午餐后", "supplement_ids": ["sup-glutamine", "sup-bcaa", "sup-gos"], "dose": "谷氨酰胺2.5g + BCAA 2.5g + GOS 2.5~5g", "notes": "温水冲服;GOS从2.5g起步观察腹胀与血糖" },
+            { "time": "17:30", "label": "晚餐后", "supplement_ids": ["sup-hean-amino", "sup-difler-boulardii"], "dose": "和安氨基酸1粒 + 迪辅乐布拉氏酵母5滴", "notes": "和安早晚各1粒;迪辅乐巩固抗菌,逐步减量" },
+            { "time": "19:30", "label": "睡前", "supplement_ids": ["sup-tryptophan", "sup-histidine"], "dose": "色氨酸半粒~1粒 + 组氨酸500mg", "notes": "温水送服;监测夜间血糖,白天嗜睡则减量" }
+          ]
+        },
+        {
+          "cycle_id": "cycle-3",
+          "time_slots": [
+            { "time": "06:00", "label": "晨起空腹", "supplement_ids": ["sup-lelichang50"], "dose": "乐力畅50+益生菌(无糖)1袋", "notes": "长期维持量,温水冲服" },
+            { "time": "07:00", "label": "早餐后", "supplement_ids": ["sup-lifeineng-iron", "sup-vitamin-c", "sup-turmeric", "sup-akkermansia", "sup-fish-oil"], "dose": "铁剂1粒(或隔日)+维C / 姜黄1粒 / Akk菌1粒 / 深海鱼油1粒", "notes": "鱼油第9周起,选80%+高纯度;监测凝血与血糖" },
+            { "time": "12:30", "label": "午餐后", "supplement_ids": ["sup-glutamine", "sup-bcaa", "sup-gos"], "dose": "谷氨酰胺2-3g + BCAA 5g + GOS 5g", "notes": "温水冲服;长期低剂量维持" },
+            { "time": "17:30", "label": "晚餐后", "supplement_ids": ["sup-hean-amino"], "dose": "和安氨基酸1粒", "notes": "饮食氨基酸充足可减为每日1粒" },
+            { "time": "19:30", "label": "睡前", "supplement_ids": ["sup-tryptophan", "sup-histidine"], "dose": "色氨酸1粒(或隔日) + 组氨酸500mg(或减量)", "notes": "温水送服;睡眠改善后减量" }
+          ]
+        }
+      ],
+      "daily_monitoring": [
+        { "time": "19:00", "name": "每日记录:排便情况(次数/性状/颜色)", "kind": "self_report" },
+        { "time": "19:00", "name": "每日记录:腹部症状(腹痛/腹泻/胀气)", "kind": "self_report" },
+        { "time": "19:00", "name": "每日足部护理(温水洗脚+检查足部)", "kind": "physical" }
+      ],
+      "weekly_monitoring": [
+        { "weekday": "周一", "name": "每周评估:食欲与食量(与血糖关系)", "indicator_ids": [] },
+        { "weekday": "周一", "name": "每周测体重1次并记录", "indicator_ids": [] },
+        { "weekday": "周三", "name": "本周测血糖2-3次(空腹+餐后2h)", "indicator_ids": ["ind-fpg", "ind-pg2h"] }
+      ],
+      "followups": [
+        { "week": "4周", "name": "复查血常规(血红蛋白,评估铁缺乏改善)", "indicator_ids": ["ind-hb"], "note": "4周后查血常规" },
+        { "week": "8-12周", "name": "复查血清铁蛋白(评估储存铁恢复)", "indicator_ids": ["ind-ferritin"], "note": "8-12周查血清铁蛋白" },
+        { "week": "12周", "name": "复查糖化血红蛋白 HbA1c", "indicator_ids": ["ind-hba1c"], "note": "每3个月查一次" },
+        { "week": "12周", "name": "复查血脂", "indicator_ids": ["ind-tc", "ind-tg", "ind-ldlc", "ind-hdlc"], "note": "12周查,评估心脑血管风险" },
+        { "week": "12周", "name": "复查肝肾功能", "indicator_ids": ["ind-alt", "ind-ast", "ind-bun", "ind-cre", "ind-egfr", "ind-ua"], "note": "12周查,评估补充剂安全性" }
+      ],
+      "exercises": ["ex-walk", "ex-toe-stand"]
+    },
+    {
+      "family_id": 90,
+      "name": "闫德波",
+      "user_id": 79,
+      "family_member_id": 41,
+      "phone": "13841386977",
+      "phases": [
+        {
+          "cycle_id": "cycle-2",
+          "time_slots": [
+            { "time": "06:00", "label": "晨起空腹", "supplement_ids": ["sup-lelichang50"], "dose": "乐力畅50+中老年益生菌 1-2袋", "notes": "温水冲服或干吃;主力补双歧+提升多样性" },
+            { "time": "07:00", "label": "早餐后", "supplement_ids": ["sup-lifeineng-iron", "sup-vitamin-c", "sup-hean-amino"], "dose": "力蜚能铁1粒+维C / 和安氨基酸1粒", "notes": "铁剂餐后+维C;和安早晚各1粒" },
+            { "time": "12:30", "label": "午餐后", "supplement_ids": ["sup-bcaa", "sup-fos"], "dose": "BCAA 2.5g + 低聚果糖FOS 5-10g", "notes": "温水冲服;益生元维持量,根据耐受调整" },
+            { "time": "17:30", "label": "晚餐后", "supplement_ids": ["sup-mixed-prebiotic", "sup-hean-amino"], "dose": "混合益生元(FOS+菊粉+抗性糊精)10-15g + 和安氨基酸1粒", "notes": "午+晚各半量;喂养内源产丁酸菌" },
+            { "time": "19:30", "label": "睡前", "supplement_ids": ["sup-tryptophan"], "dose": "色氨酸半粒~1粒(500mg)", "notes": "温水送服;改善睡眠,白天嗜睡则减量" }
+          ]
+        },
+        {
+          "cycle_id": "cycle-3",
+          "time_slots": [
+            { "time": "06:00", "label": "晨起空腹", "supplement_ids": ["sup-lelichang50"], "dose": "乐力畅50+益生菌 1袋", "notes": "长期维持量" },
+            { "time": "07:00", "label": "早餐后", "supplement_ids": ["sup-lifeineng-iron", "sup-vitamin-c", "sup-hean-amino"], "dose": "力蜚能铁1粒(或隔日)+维C / 和安氨基酸1粒", "notes": "根据复查铁蛋白结果调整" },
+            { "time": "12:30", "label": "午餐后", "supplement_ids": ["sup-bcaa", "sup-fos"], "dose": "BCAA 5g + FOS 5g", "notes": "可与BCAA一起吃" },
+            { "time": "17:30", "label": "晚餐后", "supplement_ids": ["sup-mixed-prebiotic"], "dose": "混合益生元 5-10g", "notes": "长期低剂量维持安全" },
+            { "time": "19:30", "label": "睡前", "supplement_ids": ["sup-tryptophan"], "dose": "色氨酸1粒(睡眠改善可隔日)", "notes": "温水送服" }
+          ]
+        }
+      ],
+      "daily_monitoring": [
+        { "time": "19:00", "name": "每日记录:排便情况(次数/性状/颜色)", "kind": "self_report" },
+        { "time": "19:00", "name": "每日记录:腹部症状(腹胀/腹痛/胀气)", "kind": "self_report" }
+      ],
+      "weekly_monitoring": [
+        { "weekday": "周一", "name": "每周评估:食欲与食量", "indicator_ids": [] },
+        { "weekday": "周一", "name": "每周测体重1次并记录", "indicator_ids": [] }
+      ],
+      "followups": [
+        { "week": "4周", "name": "复查血常规(血红蛋白,评估铁缺乏改善)", "indicator_ids": ["ind-hb"], "note": "4周后查血常规" },
+        { "week": "8-12周", "name": "复查血清铁蛋白(评估储存铁恢复)", "indicator_ids": ["ind-ferritin"], "note": "8-12周查血清铁蛋白" },
+        { "week": "12周", "name": "复查血脂", "indicator_ids": ["ind-tc", "ind-tg", "ind-ldlc", "ind-hdlc"], "note": "12周查" },
+        { "week": "12周", "name": "复查肝肾功能", "indicator_ids": ["ind-alt", "ind-ast", "ind-bun", "ind-cre", "ind-egfr", "ind-ua"], "note": "12周查,补充剂安全性评估" }
+      ],
+      "exercises": ["ex-walk", "ex-toe-stand"]
+    }
+  ]
+}

+ 50 - 0
docs/参考资料/知识库/schema/interaction.schema.json

@@ -0,0 +1,50 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "interaction.schema.json",
+  "title": "补剂相互作用库 Schema",
+  "type": "object",
+  "required": ["version", "updated", "interactions"],
+  "properties": {
+    "version": { "type": "string" },
+    "updated": { "type": "string", "format": "date" },
+    "interactions": {
+      "type": "array",
+      "minItems": 1,
+      "items": {
+        "type": "object",
+        "required": ["id", "type", "subjects_a", "subjects_b", "rule", "detail", "severity"],
+        "properties": {
+          "id": { "type": "string", "pattern": "^ix-[a-z0-9-]+$" },
+          "type": { "enum": ["协同", "拮抗", "时序禁忌", "安全警示"] },
+          "subjects_a": { "$ref": "#/$defs/subject" },
+          "subjects_b": { "$ref": "#/$defs/subject" },
+          "rule": { "type": "string", "examples": ["错开2小时以上", "同服促吸收", "间隔2小时"] },
+          "detail": { "type": "string" },
+          "severity": { "enum": ["提示", "警示", "禁忌"] },
+          "source": { "type": "string" }
+        },
+        "additionalProperties": false
+      }
+    }
+  },
+  "$defs": {
+    "subject": {
+      "description": "参与方:补剂引用或自由物质名(药物/食物)",
+      "oneOf": [
+        {
+          "type": "object",
+          "required": ["supplement_id"],
+          "properties": { "supplement_id": { "type": "string" } },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": ["substance"],
+          "properties": { "substance": { "type": "string" } },
+          "additionalProperties": false
+        }
+      ]
+    }
+  },
+  "additionalProperties": false
+}

+ 116 - 0
docs/参考资料/知识库/schema/protocol.schema.json

@@ -0,0 +1,116 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "protocol.schema.json",
+  "title": "干预方案库 Schema",
+  "description": "分阶段干预方案:周期(阶段)+ 成员 + 时段补剂 + 全程监测 + 运动 + 复查节点。",
+  "type": "object",
+  "required": ["version", "updated", "id", "name", "source_doc", "cycles", "members"],
+  "properties": {
+    "version": { "type": "string" },
+    "updated": { "type": "string", "format": "date" },
+    "id": { "type": "string", "pattern": "^protocol-[a-z0-9-]+$" },
+    "name": { "type": "string" },
+    "source_doc": { "type": "string" },
+    "cycles": {
+      "type": "array",
+      "minItems": 1,
+      "items": {
+        "type": "object",
+        "required": ["id", "name", "start", "end"],
+        "properties": {
+          "id": { "type": "string", "pattern": "^cycle-[a-z0-9-]+$" },
+          "name": { "type": "string" },
+          "start": { "type": "string", "format": "date" },
+          "end": { "type": "string", "format": "date" }
+        },
+        "additionalProperties": false
+      }
+    },
+    "members": {
+      "type": "array",
+      "minItems": 1,
+      "items": {
+        "type": "object",
+        "required": ["family_id", "name", "user_id", "family_member_id", "phone", "phases", "daily_monitoring", "weekly_monitoring", "followups", "exercises"],
+        "properties": {
+          "family_id": { "type": "integer" },
+          "name": { "type": "string" },
+          "user_id": { "type": "integer" },
+          "family_member_id": { "type": "integer" },
+          "phone": { "type": "string" },
+          "phases": {
+            "type": "array",
+            "minItems": 1,
+            "items": {
+              "type": "object",
+              "required": ["cycle_id", "time_slots"],
+              "properties": {
+                "cycle_id": { "type": "string" },
+                "time_slots": {
+                  "type": "array",
+                  "minItems": 1,
+                  "items": {
+                    "type": "object",
+                    "required": ["time", "label", "supplement_ids", "dose", "notes"],
+                    "properties": {
+                      "time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$" },
+                      "label": { "type": "string" },
+                      "supplement_ids": { "type": "array", "items": { "type": "string" } },
+                      "dose": { "type": "string" },
+                      "notes": { "type": "string" }
+                    },
+                    "additionalProperties": false
+                  }
+                }
+              },
+              "additionalProperties": false
+            }
+          },
+          "daily_monitoring": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "required": ["time", "name"],
+              "properties": {
+                "time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$" },
+                "name": { "type": "string" },
+                "kind": { "enum": ["self_report", "physical"] }
+              },
+              "additionalProperties": false
+            }
+          },
+          "weekly_monitoring": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "required": ["weekday", "name"],
+              "properties": {
+                "weekday": { "type": "string", "examples": ["周一", "周三"] },
+                "name": { "type": "string" },
+                "indicator_ids": { "type": "array", "items": { "type": "string" } }
+              },
+              "additionalProperties": false
+            }
+          },
+          "followups": {
+            "type": "array",
+            "items": {
+              "type": "object",
+              "required": ["week", "name"],
+              "properties": {
+                "week": { "type": "string", "examples": ["4周", "8-12周", "12周"] },
+                "name": { "type": "string" },
+                "indicator_ids": { "type": "array", "items": { "type": "string" } },
+                "note": { "type": "string" }
+              },
+              "additionalProperties": false
+            }
+          },
+          "exercises": { "type": "array", "items": { "type": "string" } }
+        },
+        "additionalProperties": false
+      }
+    }
+  },
+  "additionalProperties": false
+}