Browse Source

commit 增加香火传承及血缘传承

林海 5 days ago
parent
commit
02709d1f14
2 changed files with 318 additions and 87 deletions
  1. 239 74
      app.py
  2. 79 13
      templates/lineage_query.html

+ 239 - 74
app.py

@@ -1672,10 +1672,13 @@ def search_member():
 def get_lineage(member_id):
 def get_lineage(member_id):
     if 'user_id' not in session:
     if 'user_id' not in session:
         return jsonify({"success": False, "message": "Unauthorized"}), 401
         return jsonify({"success": False, "message": "Unauthorized"}), 401
-    
+
+    # 追溯模式:incense(香火传承,入继→养父为上辈) | blood(血脉追溯,亲生父为上辈)
+    mode = request.args.get('mode', 'incense')
+
     import time
     import time
     start_time = time.time()
     start_time = time.time()
-    print(f"[Lineage Query] Starting query for member_id: {member_id} at {time.strftime('%Y-%m-%d %H:%M:%S')}")
+    print(f"[Lineage Query] Starting query for member_id: {member_id} mode={mode} at {time.strftime('%Y-%m-%d %H:%M:%S')}")
     
     
     conn = get_db_connection()
     conn = get_db_connection()
     try:
     try:
@@ -1714,21 +1717,36 @@ def get_lineage(member_id):
                 if not parents:
                 if not parents:
                     break
                     break
                 
                 
-                # 优先选择直系父母(非出继),如果都是出继/入继,选择入继
-                parent = None
-                adoptive_parent = None
-                
+                # 分拣各类父母关系
+                normal_parent = None
+                adoptive_parent = None  # sub_type=3:入继养父
+                bio_parent = None       # sub_type=2:出继亲生父
+
                 for p in parents:
                 for p in parents:
-                    if p['sub_relation_type'] == 2:  # 出继(亲生父母)
-                        parent = p
-                    elif p['sub_relation_type'] == 3:  # 入继(养父母)
+                    if p['sub_relation_type'] == 3:
                         adoptive_parent = p
                         adoptive_parent = p
-                    else:  # 普通关系(亲生)
-                        parent = p
-                
-                # 如果没有找到普通父母,使用入继父母
-                if not parent:
-                    parent = adoptive_parent
+                    elif p['sub_relation_type'] == 2:
+                        bio_parent = p
+                    else:
+                        normal_parent = p
+
+                if mode == 'blood':
+                    # 血脉追溯:亲生父优先(出继亦沿亲生路径)
+                    parent = normal_parent or bio_parent or adoptive_parent
+                else:
+                    # 香火传承(默认):入继养父优先
+                    parent = adoptive_parent or normal_parent or bio_parent
+                    # 若走入继路径,在当事人卡片标注"从xx出继"
+                    if parent is adoptive_parent and adoptive_parent is not None:
+                        bio_name = (bio_parent.get('simplified_name') or bio_parent.get('name')) if bio_parent else None
+                        adopt_label = f"从{bio_name}出继" if bio_name else "出继"
+                        if depth == 0:
+                            center['adoption_label'] = adopt_label
+                        elif generations:
+                            generations[-1]['ancestor']['adoption_label'] = adopt_label
+
+                # 祖先卡片不携带子辈关系类型(避免把子的出继/入继标在父身上)
+                parent['sub_relation_type'] = None
 
 
                 # 循环检测:如果该祖先已在链中出现过,终止(数据异常保护)
                 # 循环检测:如果该祖先已在链中出现过,终止(数据异常保护)
                 if parent['id'] in visited_ancestor_ids:
                 if parent['id'] in visited_ancestor_ids:
@@ -1745,6 +1763,7 @@ def get_lineage(member_id):
                     FROM family_relation_info r
                     FROM family_relation_info r
                     JOIN family_member_info gp ON r.parent_mid = gp.id
                     JOIN family_member_info gp ON r.parent_mid = gp.id
                     WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
                     WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
+                    ORDER BY CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END, r.id
                     LIMIT 1
                     LIMIT 1
                 """, (parent['id'],))
                 """, (parent['id'],))
                 grandparent = cursor.fetchone()
                 grandparent = cursor.fetchone()
@@ -1761,11 +1780,12 @@ def get_lineage(member_id):
                     co_row = cursor.fetchone()
                     co_row = cursor.fetchone()
                     parent['child_order'] = co_row['child_order'] if co_row else None
                     parent['child_order'] = co_row['child_order'] if co_row else None
 
 
-                    # 获取祖先的兄弟(含 child_order,用于前端排序与徽章
+                    # 获取祖先的兄弟(含 child_order 和 sub_relation_type,用于出继/入继标注
                     cursor.execute("""
                     cursor.execute("""
                         SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
                         SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
                                EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
                                EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
-                               COALESCE(r.child_order, NULL) AS child_order
+                               COALESCE(r.child_order, NULL) AS child_order,
+                               r.sub_relation_type
                         FROM family_relation_info r
                         FROM family_relation_info r
                         JOIN family_member_info c ON r.child_mid = c.id
                         JOIN family_member_info c ON r.child_mid = c.id
                         WHERE r.parent_mid = %s AND r.relation_type IN (1, 2) AND c.id != %s
                         WHERE r.parent_mid = %s AND r.relation_type IN (1, 2) AND c.id != %s
@@ -1773,7 +1793,20 @@ def get_lineage(member_id):
                         LIMIT 30
                         LIMIT 30
                     """, (grandparent['id'], parent['id']))
                     """, (grandparent['id'], parent['id']))
                     parent_siblings = cursor.fetchall()
                     parent_siblings = cursor.fetchall()
-                
+                    # 为入继兄弟补充"从xx出继"标注
+                    for sib in parent_siblings:
+                        if sib.get('sub_relation_type') == 3:
+                            cursor.execute("""
+                                SELECT p.simplified_name, p.name
+                                FROM family_relation_info r
+                                JOIN family_member_info p ON r.parent_mid = p.id
+                                WHERE r.child_mid = %s AND r.sub_relation_type = 2 LIMIT 1
+                            """, (sib['id'],))
+                            sbp = cursor.fetchone()
+                            sib['adoption_label'] = (
+                                f"从{sbp['simplified_name'] or sbp['name']}出继" if sbp else "出继"
+                            )
+
                 # Mark sibling IDs as displayed
                 # Mark sibling IDs as displayed
                 for sibling in parent_siblings:
                 for sibling in parent_siblings:
                     displayed_ids.add(sibling['id'])
                     displayed_ids.add(sibling['id'])
@@ -1809,11 +1842,21 @@ def get_lineage(member_id):
             # Step 3: Get immediate children only (limited count)
             # Step 3: Get immediate children only (limited count)
             step_start = time.time()
             step_start = time.time()
             
             
-            # 获取子女:
-            #   - 包含入继子女(sub_relation_type=3,养父母侧)
-            #   - 包含普通子女(sub_relation_type 为空或非2/3)
-            #   - 排除出继子女(sub_relation_type=2,生父母侧)若该子女已有养父母记录
-            cursor.execute("""
+            # 获取子女:根据模式选择不同过滤策略
+            # 香火传承:包含入继子女、普通子女,排除已被人收继的出继子女
+            # 血脉追溯:包含亲生子女(含出继走的),排除从别处入继的子女
+            if mode == 'blood':
+                children_filter = "AND COALESCE(r.sub_relation_type, 0) != 3"
+            else:
+                children_filter = """AND (
+                    COALESCE(r.sub_relation_type, 0) != 2
+                    OR NOT EXISTS (
+                      SELECT 1 FROM family_relation_info r2
+                      WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
+                    )
+                  )"""
+
+            cursor.execute(f"""
                 SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
                 SELECT c.id, c.name, c.simplified_name, c.name_word, c.name_word_generation,
                        EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
                        EXISTS(SELECT 1 FROM family_relation_info WHERE parent_mid = c.id AND relation_type IN (1, 2)) as has_children,
                        r.sub_relation_type,
                        r.sub_relation_type,
@@ -1821,36 +1864,37 @@ def get_lineage(member_id):
                 FROM family_relation_info r
                 FROM family_relation_info r
                 JOIN family_member_info c ON r.child_mid = c.id
                 JOIN family_member_info c ON r.child_mid = c.id
                 WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
                 WHERE r.parent_mid = %s AND r.relation_type IN (1, 2)
-                  AND (
-                    COALESCE(r.sub_relation_type, 0) != 2
-                    OR NOT EXISTS (
-                      SELECT 1 FROM family_relation_info r2
-                      WHERE r2.child_mid = c.id AND r2.sub_relation_type = 3
-                    )
-                  )
+                  {children_filter}
                 ORDER BY COALESCE(r.child_order, 99999), c.id
                 ORDER BY COALESCE(r.child_order, 99999), c.id
                 LIMIT 30
                 LIMIT 30
             """, (member_id,))
             """, (member_id,))
             children = cursor.fetchall()
             children = cursor.fetchall()
 
 
-            # 对于入继的子女,获取其生父母信息并生成"由xxx公第N子入继"说明
-            _order_labels_lg = {1:'长', 2:'次', 3:'三', 4:'四', 5:'五',
-                                6:'六', 7:'七', 8:'八', 9:'九', 10:'十'}
+            # 香火模式:对入继子女标注"从xx出继";血脉模式:对出继子女标注"出继至xx"
             for child in children:
             for child in children:
-                if child['sub_relation_type'] == 3:  # 入继
+                if mode == 'incense' and child['sub_relation_type'] == 3:
                     cursor.execute("""
                     cursor.execute("""
-                        SELECT p.id, p.name, p.simplified_name, r.child_order
+                        SELECT p.simplified_name, p.name
                         FROM family_relation_info r
                         FROM family_relation_info r
                         JOIN family_member_info p ON r.parent_mid = p.id
                         JOIN family_member_info p ON r.parent_mid = p.id
-                        WHERE r.child_mid = %s AND r.sub_relation_type = 2
-                        LIMIT 1
+                        WHERE r.child_mid = %s AND r.sub_relation_type = 2 LIMIT 1
                     """, (child['id'],))
                     """, (child['id'],))
-                    bio_parent = cursor.fetchone()
-                    if bio_parent:
-                        bio_name = bio_parent['simplified_name'] or bio_parent['name']
-                        order = bio_parent['child_order']
-                        order_str = _order_labels_lg.get(order, f'第{order}') if order else '某'
-                        child['adopt_info'] = f"由{bio_name}公{order_str}子入继"
+                    bio_p = cursor.fetchone()
+                    child['adoption_label'] = (
+                        f"从{bio_p['simplified_name'] or bio_p['name']}出继" if bio_p else "出继"
+                    )
+                elif mode == 'blood' and child['sub_relation_type'] == 2:
+                    # 血脉模式下标注出继子女的去向(养父)
+                    cursor.execute("""
+                        SELECT p.simplified_name, p.name
+                        FROM family_relation_info r
+                        JOIN family_member_info p ON r.parent_mid = p.id
+                        WHERE r.child_mid = %s AND r.sub_relation_type = 3 LIMIT 1
+                    """, (child['id'],))
+                    adop_p = cursor.fetchone()
+                    if adop_p:
+                        child['adoption_label'] = f"出继至{adop_p['simplified_name'] or adop_p['name']}"
+                    child['sub_relation_type'] = 2  # 保持供前端显示 adopted-out 样式
             
             
             # Initialize children array
             # Initialize children array
             for child in children:
             for child in children:
@@ -1885,6 +1929,20 @@ def get_lineage(member_id):
                     LIMIT 30
                     LIMIT 30
                 """, (parent_id, member_id))
                 """, (parent_id, member_id))
                 siblings = cursor.fetchall()
                 siblings = cursor.fetchall()
+                # 为入继兄弟补充"从xx出继"标注
+                for sib in siblings:
+                    if sib.get('sub_relation_type') == 3:
+                        cursor.execute("""
+                            SELECT p.simplified_name, p.name
+                            FROM family_relation_info r
+                            JOIN family_member_info p ON r.parent_mid = p.id
+                            WHERE r.child_mid = %s AND r.sub_relation_type = 2 LIMIT 1
+                        """, (sib['id'],))
+                        sbp = cursor.fetchone()
+                        if sbp:
+                            sib['adoption_label'] = f"从{sbp['simplified_name'] or sbp['name']}出继"
+                        else:
+                            sib['adoption_label'] = "出继"
             print(f"[Lineage Query] Step 4 - Get siblings ({len(siblings)}): {time.time() - step_start:.3f}s")
             print(f"[Lineage Query] Step 4 - Get siblings ({len(siblings)}): {time.time() - step_start:.3f}s")
             
             
             total_time = time.time() - start_time
             total_time = time.time() - start_time
@@ -1925,6 +1983,8 @@ def get_ancestors_above(ancestor_id):
     if 'user_id' not in session:
     if 'user_id' not in session:
         return jsonify({"success": False, "message": "Unauthorized"}), 401
         return jsonify({"success": False, "message": "Unauthorized"}), 401
 
 
+    mode = request.args.get('mode', 'incense')
+
     conn = get_db_connection()
     conn = get_db_connection()
     try:
     try:
         with conn.cursor() as cursor:
         with conn.cursor() as cursor:
@@ -1933,6 +1993,26 @@ def get_ancestors_above(ancestor_id):
             max_depth = 100
             max_depth = 100
             visited_ids = set([ancestor_id])
             visited_ids = set([ancestor_id])
 
 
+            # 计算 anchor 节点(ancestor_id)自身的 adoption_label(已在上层渲染,此处只补充标签)
+            anchor_adoption_label = None
+            cursor.execute("""
+                SELECT p.id, p.name, p.simplified_name, r.sub_relation_type
+                FROM family_relation_info r
+                JOIN family_member_info p ON r.parent_mid = p.id
+                WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
+            """, (ancestor_id,))
+            anchor_parents = cursor.fetchall()
+            anchor_bio = None
+            has_adoptive = False
+            for ap in anchor_parents:
+                if ap['sub_relation_type'] == 3:
+                    has_adoptive = True
+                elif ap['sub_relation_type'] == 2:
+                    anchor_bio = ap
+            if has_adoptive and anchor_bio:
+                bio_name = anchor_bio.get('simplified_name') or anchor_bio.get('name')
+                anchor_adoption_label = f"从{bio_name}出继" if bio_name else "出继"
+
             for depth in range(max_depth):
             for depth in range(max_depth):
                 cursor.execute("""
                 cursor.execute("""
                     SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation,
                     SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation,
@@ -1947,25 +2027,45 @@ def get_ancestors_above(ancestor_id):
                 if not parents:
                 if not parents:
                     break
                     break
 
 
-                parent = None
+                # 分拣各类父母关系
+                normal_parent = None
                 adoptive_parent = None
                 adoptive_parent = None
+                bio_parent = None
                 for p in parents:
                 for p in parents:
                     if p['sub_relation_type'] == 3:
                     if p['sub_relation_type'] == 3:
                         adoptive_parent = p
                         adoptive_parent = p
+                    elif p['sub_relation_type'] == 2:
+                        bio_parent = p
                     else:
                     else:
-                        parent = p
-                if not parent:
-                    parent = adoptive_parent
+                        normal_parent = p
+
+                if mode == 'blood':
+                    parent = normal_parent or bio_parent or adoptive_parent
+                else:
+                    parent = adoptive_parent or normal_parent or bio_parent
+                    # 若走入继路径,在 current_id 对应的人物上标注"从xx出继"
+                    if parent is adoptive_parent and adoptive_parent is not None:
+                        bio_name = (bio_parent.get('simplified_name') or bio_parent.get('name')) if bio_parent else None
+                        adopt_label = f"从{bio_name}出继" if bio_name else "出继"
+                        if depth == 0:
+                            anchor_adoption_label = adopt_label
+                        elif generations:
+                            generations[-1]['ancestor']['adoption_label'] = adopt_label
+
+                # 祖先卡片不携带子辈关系类型
+                parent['sub_relation_type'] = None
 
 
                 if parent['id'] in visited_ids:
                 if parent['id'] in visited_ids:
                     break
                     break
                 visited_ids.add(parent['id'])
                 visited_ids.add(parent['id'])
 
 
-                # 查祖父,用于获取该祖先的兄弟
+                # 查祖父,用于获取该祖先的兄弟(优先亲生父母,排除养父)
                 cursor.execute("""
                 cursor.execute("""
                     SELECT gp.id FROM family_relation_info r
                     SELECT gp.id FROM family_relation_info r
                     JOIN family_member_info gp ON r.parent_mid = gp.id
                     JOIN family_member_info gp ON r.parent_mid = gp.id
-                    WHERE r.child_mid = %s AND r.relation_type IN (1, 2) LIMIT 1
+                    WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
+                    ORDER BY CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END, r.id
+                    LIMIT 1
                 """, (parent['id'],))
                 """, (parent['id'],))
                 grandparent = cursor.fetchone()
                 grandparent = cursor.fetchone()
 
 
@@ -2020,7 +2120,8 @@ def get_ancestors_above(ancestor_id):
                 "data": {
                 "data": {
                     "generations": generations,
                     "generations": generations,
                     "has_more_ancestors": has_more_ancestors,
                     "has_more_ancestors": has_more_ancestors,
-                    "topmost_ancestor_id": topmost_ancestor_id
+                    "topmost_ancestor_id": topmost_ancestor_id,
+                    "anchor_adoption_label": anchor_adoption_label
                 }
                 }
             })
             })
     except Exception as e:
     except Exception as e:
@@ -5832,6 +5933,8 @@ def api_get_lineage(member_id):
     if not token:
     if not token:
         return jsonify({"success": False, "message": "未登录"}), 401
         return jsonify({"success": False, "message": "未登录"}), 401
 
 
+    mode = request.args.get('mode', 'incense')
+
     conn = get_db_connection()
     conn = get_db_connection()
     try:
     try:
         with conn.cursor() as cursor:
         with conn.cursor() as cursor:
@@ -5864,25 +5967,45 @@ def api_get_lineage(member_id):
                 if not parents:
                 if not parents:
                     break
                     break
 
 
-                # 优先取非养父母关系
-                parent = None
+                # 分拣各类父母关系
+                normal_parent = None
+                adoptive_parent = None
+                bio_parent = None
                 for p in parents:
                 for p in parents:
-                    if p['sub_relation_type'] != 3:
-                        parent = p
-                        break
-                if not parent:
-                    parent = parents[0]
+                    if p['sub_relation_type'] == 3:
+                        adoptive_parent = p
+                    elif p['sub_relation_type'] == 2:
+                        bio_parent = p
+                    else:
+                        normal_parent = p
+
+                if mode == 'blood':
+                    parent = normal_parent or bio_parent or adoptive_parent
+                else:
+                    parent = adoptive_parent or normal_parent or bio_parent
+                    if parent is adoptive_parent and adoptive_parent is not None:
+                        bio_name = (bio_parent.get('simplified_name') or bio_parent.get('name')) if bio_parent else None
+                        adopt_label = f"从{bio_name}出继" if bio_name else "出继"
+                        if depth == 0:
+                            center['adoption_label'] = adopt_label
+                        elif generations:
+                            generations[-1]['ancestor']['adoption_label'] = adopt_label
+
+                # 祖先卡片不携带子辈关系类型
+                parent['sub_relation_type'] = None
 
 
                 # 循环检测
                 # 循环检测
                 if parent['id'] in visited_ancestor_ids:
                 if parent['id'] in visited_ancestor_ids:
                     break
                     break
                 visited_ancestor_ids.add(parent['id'])
                 visited_ancestor_ids.add(parent['id'])
 
 
-                # 查祖父以获取该祖先的兄弟
+                # 查祖父以获取该祖先的兄弟(优先亲生父母,排除养父)
                 cursor.execute("""
                 cursor.execute("""
                     SELECT gp.id FROM family_relation_info r
                     SELECT gp.id FROM family_relation_info r
                     JOIN family_member_info gp ON r.parent_mid = gp.id
                     JOIN family_member_info gp ON r.parent_mid = gp.id
-                    WHERE r.child_mid = %s AND r.relation_type IN (1, 2) LIMIT 1
+                    WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
+                    ORDER BY CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END, r.id
+                    LIMIT 1
                 """, (parent['id'],))
                 """, (parent['id'],))
                 grandparent = cursor.fetchone()
                 grandparent = cursor.fetchone()
 
 
@@ -5943,14 +6066,12 @@ def api_get_lineage(member_id):
                 LIMIT 20
                 LIMIT 20
             """, (member_id,))
             """, (member_id,))
             children = cursor.fetchall()
             children = cursor.fetchall()
-            _order_labels_alg = {1:'长', 2:'次', 3:'三', 4:'四', 5:'五',
-                                 6:'六', 7:'七', 8:'八', 9:'九', 10:'十'}
             for c in children:
             for c in children:
                 c['has_children'] = bool(c['has_children'])
                 c['has_children'] = bool(c['has_children'])
-                # 入继子女:附加生父母信息,生成"由xxx公第N子入继"说明
+                # 入继子女:附加生父母信息,生成"从xx出继"标注
                 if c['sub_relation_type'] == 3:
                 if c['sub_relation_type'] == 3:
                     cursor.execute("""
                     cursor.execute("""
-                        SELECT p.name, p.simplified_name, r.child_order
+                        SELECT p.name, p.simplified_name
                         FROM family_relation_info r
                         FROM family_relation_info r
                         JOIN family_member_info p ON r.parent_mid = p.id
                         JOIN family_member_info p ON r.parent_mid = p.id
                         WHERE r.child_mid = %s AND r.sub_relation_type = 2 LIMIT 1
                         WHERE r.child_mid = %s AND r.sub_relation_type = 2 LIMIT 1
@@ -5958,9 +6079,9 @@ def api_get_lineage(member_id):
                     bp = cursor.fetchone()
                     bp = cursor.fetchone()
                     if bp:
                     if bp:
                         bio_name = bp['simplified_name'] or bp['name']
                         bio_name = bp['simplified_name'] or bp['name']
-                        order = bp['child_order']
-                        order_str = _order_labels_alg.get(order, f'第{order}') if order else '某'
-                        c['adopt_info'] = f"由{bio_name}公{order_str}子入继"
+                        c['adoption_label'] = f"从{bio_name}出继"
+                    else:
+                        c['adoption_label'] = "出继"
 
 
             # Step 4: 获取查询人物的同辈兄弟(含center自己的child_order)
             # Step 4: 获取查询人物的同辈兄弟(含center自己的child_order)
             siblings = []
             siblings = []
@@ -6027,6 +6148,8 @@ def api_get_ancestors_above(ancestor_id):
     if not token:
     if not token:
         return jsonify({"success": False, "message": "未登录"}), 401
         return jsonify({"success": False, "message": "未登录"}), 401
 
 
+    mode = request.args.get('mode', 'incense')
+
     conn = get_db_connection()
     conn = get_db_connection()
     try:
     try:
         with conn.cursor() as cursor:
         with conn.cursor() as cursor:
@@ -6035,6 +6158,26 @@ def api_get_ancestors_above(ancestor_id):
             max_depth = 100
             max_depth = 100
             visited_ids = set([ancestor_id])
             visited_ids = set([ancestor_id])
 
 
+            # 计算 anchor 节点(ancestor_id)自身的 adoption_label
+            anchor_adoption_label_wx = None
+            cursor.execute("""
+                SELECT p.id, p.name, p.simplified_name, r.sub_relation_type
+                FROM family_relation_info r
+                JOIN family_member_info p ON r.parent_mid = p.id
+                WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
+            """, (ancestor_id,))
+            anchor_parents_wx = cursor.fetchall()
+            anchor_bio_wx = None
+            has_adoptive_wx = False
+            for ap in anchor_parents_wx:
+                if ap['sub_relation_type'] == 3:
+                    has_adoptive_wx = True
+                elif ap['sub_relation_type'] == 2:
+                    anchor_bio_wx = ap
+            if has_adoptive_wx and anchor_bio_wx:
+                bio_name_wx = anchor_bio_wx.get('simplified_name') or anchor_bio_wx.get('name')
+                anchor_adoption_label_wx = f"从{bio_name_wx}出继" if bio_name_wx else "出继"
+
             for depth in range(max_depth):
             for depth in range(max_depth):
                 cursor.execute("""
                 cursor.execute("""
                     SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation,
                     SELECT p.id, p.name, p.simplified_name, p.name_word, p.name_word_generation,
@@ -6049,13 +6192,32 @@ def api_get_ancestors_above(ancestor_id):
                 if not parents:
                 if not parents:
                     break
                     break
 
 
-                parent = None
+                # 分拣各类父母关系
+                normal_parent = None
+                adoptive_parent = None
+                bio_parent = None
                 for p in parents:
                 for p in parents:
-                    if p['sub_relation_type'] != 3:
-                        parent = p
-                        break
-                if not parent:
-                    parent = parents[0]
+                    if p['sub_relation_type'] == 3:
+                        adoptive_parent = p
+                    elif p['sub_relation_type'] == 2:
+                        bio_parent = p
+                    else:
+                        normal_parent = p
+
+                if mode == 'blood':
+                    parent = normal_parent or bio_parent or adoptive_parent
+                else:
+                    parent = adoptive_parent or normal_parent or bio_parent
+                    if parent is adoptive_parent and adoptive_parent is not None:
+                        bio_name = (bio_parent.get('simplified_name') or bio_parent.get('name')) if bio_parent else None
+                        adopt_label = f"从{bio_name}出继" if bio_name else "出继"
+                        if depth == 0:
+                            anchor_adoption_label_wx = adopt_label
+                        elif generations:
+                            generations[-1]['ancestor']['adoption_label'] = adopt_label
+
+                # 祖先卡片不携带子辈关系类型
+                parent['sub_relation_type'] = None
 
 
                 if parent['id'] in visited_ids:
                 if parent['id'] in visited_ids:
                     break
                     break
@@ -6064,7 +6226,9 @@ def api_get_ancestors_above(ancestor_id):
                 cursor.execute("""
                 cursor.execute("""
                     SELECT gp.id FROM family_relation_info r
                     SELECT gp.id FROM family_relation_info r
                     JOIN family_member_info gp ON r.parent_mid = gp.id
                     JOIN family_member_info gp ON r.parent_mid = gp.id
-                    WHERE r.child_mid = %s AND r.relation_type IN (1, 2) LIMIT 1
+                    WHERE r.child_mid = %s AND r.relation_type IN (1, 2)
+                    ORDER BY CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END, r.id
+                    LIMIT 1
                 """, (parent['id'],))
                 """, (parent['id'],))
                 grandparent = cursor.fetchone()
                 grandparent = cursor.fetchone()
 
 
@@ -6118,7 +6282,8 @@ def api_get_ancestors_above(ancestor_id):
             "data": {
             "data": {
                 "generations": generations,
                 "generations": generations,
                 "has_more_ancestors": has_more_ancestors,
                 "has_more_ancestors": has_more_ancestors,
-                "topmost_ancestor_id": topmost_ancestor_id
+                "topmost_ancestor_id": topmost_ancestor_id,
+                "anchor_adoption_label": anchor_adoption_label_wx
             }
             }
         })
         })
     except Exception as e:
     except Exception as e:

+ 79 - 13
templates/lineage_query.html

@@ -461,6 +461,8 @@
     .tree-node.sibling-node .node-info {
     .tree-node.sibling-node .node-info {
         font-size: 11px;
         font-size: 11px;
     }
     }
+
+    /* 世系模式开关:所有样式已内联到HTML元素上 */
 </style>
 </style>
 {% endblock %}
 {% endblock %}
 
 
@@ -471,12 +473,26 @@
         <div class="col-md-12">
         <div class="col-md-12">
             <div class="d-flex justify-content-between align-items-center">
             <div class="d-flex justify-content-between align-items-center">
                 <h2 class="page-header">世系查询</h2>
                 <h2 class="page-header">世系查询</h2>
-                <div class="search-box">
-                    <div class="input-group">
-                        <input type="text" id="searchInput" class="form-control" placeholder="搜索成员姓名(支持简繁)" />
-                        <button class="btn btn-primary" onclick="searchMember()">
-                            <i class="bi bi-search"></i>
-                        </button>
+                <div class="d-flex align-items-center" style="gap:10px;">
+                    <!-- 世系追溯模式:左右胶囊开关 -->
+                    <div id="lineageModeSwitch" style="display:inline-flex;align-items:center;background:#e5e7eb;border-radius:24px;padding:3px;gap:2px;flex-shrink:0;">
+                        <button id="modeIncense"
+                            onclick="setLineageMode('incense')"
+                            title="入继人员以养父为上辈,沿宗族香火追溯"
+                            style="padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;background:#f59e0b;color:#1a1a2e;box-shadow:0 2px 8px rgba(245,158,11,0.5);">香火传承</button>
+                        <button id="modeBlood"
+                            onclick="setLineageMode('blood')"
+                            title="出继人员以亲生父为上辈,沿血缘源流追溯"
+                            style="padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;background:transparent;color:#6b7280;">血缘传承</button>
+                    </div>
+                    <!-- 搜索框 -->
+                    <div class="search-box">
+                        <div class="input-group">
+                            <input type="text" id="searchInput" class="form-control" placeholder="搜索成员姓名(支持简繁)" />
+                            <button class="btn btn-primary" onclick="searchMember()">
+                                <i class="bi bi-search"></i>
+                            </button>
+                        </div>
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>
@@ -677,6 +693,7 @@ function selectMemberByIndex(index) {
 
 
 // Load lineage data
 // Load lineage data
 async function loadLineage(memberId) {
 async function loadLineage(memberId) {
+    _currentMemberId = memberId;
     // Show loading state - preserve container elements
     // Show loading state - preserve container elements
     document.getElementById('ancestorsTree').innerHTML = `
     document.getElementById('ancestorsTree').innerHTML = `
         <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
         <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px;">
@@ -704,7 +721,7 @@ async function loadLineage(memberId) {
     `;
     `;
     
     
     try {
     try {
-        const response = await fetch(`/manager/api/get_lineage/${memberId}`, {
+        const response = await fetch(`/manager/api/get_lineage/${memberId}?mode=${_lineageMode}`, {
             credentials: 'include'
             credentials: 'include'
         });
         });
         
         
@@ -761,15 +778,24 @@ function renderNode(person, type) {
     // siblings / children use default style (can be slightly smaller)
     // siblings / children use default style (can be slightly smaller)
     if (type === 'sibling') cls += ' sibling-node';
     if (type === 'sibling') cls += ' sibling-node';
 
 
-    if (person.sub_relation_type === 2) cls += ' adopted-out';
-    else if (person.sub_relation_type === 3) cls += ' adopted-in';
+    // adoption_label:后端直接标注在当事人身上(如"从xx出继"),优先使用
+    // sub_relation_type 仅用于子女/入继节点的补充显示
+    if (person.adoption_label) {
+        cls += ' adopted-out';
+    } else if (person.sub_relation_type === 2) {
+        cls += ' adopted-out';
+    } else if (person.sub_relation_type === 3) {
+        cls += ' adopted-in';
+    }
 
 
     let adoptLabel = '';
     let adoptLabel = '';
-    if (person.sub_relation_type === 2) {
-        // 出继(生父母侧记录,通常不再显示,但保留以防万一)
+    if (person.adoption_label) {
+        // 后端已计算好的标注(用于出继/入继当事人本人卡片)
+        adoptLabel = `<div class="adoption-label">${person.adoption_label}</div>`;
+    } else if (person.sub_relation_type === 2) {
         adoptLabel = `<div class="adoption-label">${person.adoptive_parent_name ? '出继给 ' + person.adoptive_parent_name : '出继'}</div>`;
         adoptLabel = `<div class="adoption-label">${person.adoptive_parent_name ? '出继给 ' + person.adoptive_parent_name : '出继'}</div>`;
     } else if (person.sub_relation_type === 3) {
     } else if (person.sub_relation_type === 3) {
-        // 入继(养父母侧记录):优先显示完整说明"由xxx公第N子入继"
+        // 入继(养父母侧子女记录):优先显示完整说明
         const adoptText = person.adopt_info || (person.bio_parent_name ? `入继自 ${person.bio_parent_name}` : '入继');
         const adoptText = person.adopt_info || (person.bio_parent_name ? `入继自 ${person.bio_parent_name}` : '入继');
         adoptLabel = `<div class="adoption-label adopted-in-label">${adoptText}</div>`;
         adoptLabel = `<div class="adoption-label adopted-in-label">${adoptText}</div>`;
     }
     }
@@ -830,6 +856,39 @@ let _currentGenerations = [];
 let _currentCenter = null;
 let _currentCenter = null;
 let _currentSiblings = [];
 let _currentSiblings = [];
 let _currentChildren = [];
 let _currentChildren = [];
+let _currentMemberId = null;    // 记录当前查询人员,切换模式时重新加载
+let _lineageMode = 'incense';   // 'incense':香火传承(养父为上辈) | 'blood':血脉追溯(亲生父为上辈)
+
+// 切换世系追溯模式(element.style 直接控制,最高优先级,不受CSS影响)
+function setLineageMode(mode) {
+    const changed = (_lineageMode !== mode);
+    _lineageMode = mode;
+
+    const btnI = document.getElementById('modeIncense');
+    const btnB = document.getElementById('modeBlood');
+
+    // 页面 header 背景是浅色,用深色方案确保可见
+    if (mode === 'incense') {
+        if (btnI) {
+            btnI.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;background:#f59e0b;color:#1a1a2e;box-shadow:0 2px 8px rgba(245,158,11,0.5);';
+        }
+        if (btnB) {
+            btnB.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;background:transparent;color:#6b7280;box-shadow:none;';
+        }
+    } else {
+        if (btnI) {
+            btnI.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;background:transparent;color:#6b7280;box-shadow:none;';
+        }
+        if (btnB) {
+            btnB.style.cssText = 'padding:6px 20px;border:none;border-radius:20px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;background:#ef4444;color:#ffffff;box-shadow:0 2px 8px rgba(239,68,68,0.45);';
+        }
+    }
+
+    // 模式变化且已有查询人员时,重新加载世系
+    if (changed && _currentMemberId) {
+        loadLineage(_currentMemberId);
+    }
+}
 
 
 function renderLineage(data) {
 function renderLineage(data) {
     const { center, generations, siblings, children } = data;
     const { center, generations, siblings, children } = data;
@@ -989,7 +1048,7 @@ async function loadMoreAncestors(topmostAncestorId, btn) {
         btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> 追溯中...';
         btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> 追溯中...';
     }
     }
     try {
     try {
-        const resp = await fetch(`/manager/api/get_ancestors_above/${topmostAncestorId}`, {
+        const resp = await fetch(`/manager/api/get_ancestors_above/${topmostAncestorId}?mode=${_lineageMode}`, {
             credentials: 'include'
             credentials: 'include'
         });
         });
         const result = await resp.json();
         const result = await resp.json();
@@ -1003,6 +1062,13 @@ async function loadMoreAncestors(topmostAncestorId, btn) {
             if (btn) btn.outerHTML = '<div style="font-size:12px;color:rgba(255,255,255,0.3);text-align:center;padding:6px 0;">↑ 已到达最上辈先祖</div>';
             if (btn) btn.outerHTML = '<div style="font-size:12px;color:rgba(255,255,255,0.3);text-align:center;padding:6px 0;">↑ 已到达最上辈先祖</div>';
             return;
             return;
         }
         }
+        // 若 anchor 节点本身有出继标注,更新其在 _currentGenerations 中的记录
+        if (result.data.anchor_adoption_label) {
+            const anchorEntry = _currentGenerations.find(g => g.ancestor && g.ancestor.id === topmostAncestorId);
+            if (anchorEntry) {
+                anchorEntry.ancestor.adoption_label = result.data.anchor_adoption_label;
+            }
+        }
         // 将新祖先追加到全局 _currentGenerations 末尾(末尾 = 更远的祖先)
         // 将新祖先追加到全局 _currentGenerations 末尾(末尾 = 更远的祖先)
         _currentGenerations = _currentGenerations.concat(newGens);
         _currentGenerations = _currentGenerations.concat(newGens);
         // 重新渲染整个视图(中心人物、兄弟、子女使用缓存数据)
         // 重新渲染整个视图(中心人物、兄弟、子女使用缓存数据)