|
|
@@ -1846,12 +1846,17 @@ def get_lineage(member_id):
|
|
|
|
|
|
# Get siblings of this ancestor (father's brothers)
|
|
|
# First get grandparent (parent's father)
|
|
|
- cursor.execute("""
|
|
|
+ # 香火传承:优先选养父(sub_type=3);血脉传承:优先选生父(sub_type=0/2)
|
|
|
+ if mode == 'incense':
|
|
|
+ gp_order = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
|
|
|
+ else:
|
|
|
+ gp_order = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
|
|
|
+ cursor.execute(f"""
|
|
|
SELECT gp.id
|
|
|
FROM family_relation_info r
|
|
|
JOIN family_member_info gp ON r.parent_mid = gp.id
|
|
|
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
|
|
|
+ ORDER BY {gp_order}, r.id
|
|
|
LIMIT 1
|
|
|
""", (parent['id'],))
|
|
|
grandparent = cursor.fetchone()
|
|
|
@@ -2147,12 +2152,17 @@ def get_ancestors_above(ancestor_id):
|
|
|
break
|
|
|
visited_ids.add(parent['id'])
|
|
|
|
|
|
- # 查祖父,用于获取该祖先的兄弟(优先亲生父母,排除养父)
|
|
|
- cursor.execute("""
|
|
|
+ # 查祖父,用于获取该祖先的兄弟
|
|
|
+ # 香火传承:优先养父(sub_type=3);血脉传承:优先生父
|
|
|
+ if mode == 'incense':
|
|
|
+ gp_order_ab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
|
|
|
+ else:
|
|
|
+ gp_order_ab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
|
|
|
+ cursor.execute(f"""
|
|
|
SELECT gp.id FROM family_relation_info r
|
|
|
JOIN family_member_info gp ON r.parent_mid = gp.id
|
|
|
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
|
|
|
+ ORDER BY {gp_order_ab}, r.id
|
|
|
LIMIT 1
|
|
|
""", (parent['id'],))
|
|
|
grandparent = cursor.fetchone()
|
|
|
@@ -2905,15 +2915,16 @@ def add_member():
|
|
|
relations = []
|
|
|
# Parse relations from form data
|
|
|
i = 0
|
|
|
- while True:
|
|
|
+ while i < 50:
|
|
|
parent_mid = request.form.get(f'relations[{i}][parent_mid]')
|
|
|
- rel_type = request.form.get(f'relations[{i}][relation_type]')
|
|
|
- sub_rel_type = request.form.get(f'relations[{i}][sub_relation_type]', '0')
|
|
|
- child_order_raw = request.form.get(f'relations[{i}][child_order]', '')
|
|
|
-
|
|
|
- if not parent_mid or not rel_type:
|
|
|
+ rel_type = request.form.get(f'relations[{i}][relation_type]')
|
|
|
+ if parent_mid is None and rel_type is None:
|
|
|
break
|
|
|
-
|
|
|
+ i += 1
|
|
|
+ if not parent_mid or not rel_type:
|
|
|
+ continue
|
|
|
+ sub_rel_type = request.form.get(f'relations[{i-1}][sub_relation_type]', '0')
|
|
|
+ child_order_raw = request.form.get(f'relations[{i-1}][child_order]', '')
|
|
|
child_order = int(child_order_raw) if child_order_raw.strip().isdigit() else None
|
|
|
relations.append({
|
|
|
'parent_mid': int(parent_mid),
|
|
|
@@ -2921,8 +2932,7 @@ def add_member():
|
|
|
'sub_relation_type': int(sub_rel_type),
|
|
|
'child_order': child_order
|
|
|
})
|
|
|
- i += 1
|
|
|
-
|
|
|
+
|
|
|
# For backward compatibility, check old-style single relation
|
|
|
if not relations:
|
|
|
related_mid = request.form.get('related_mid')
|
|
|
@@ -3073,9 +3083,14 @@ def add_member():
|
|
|
print(f"[Add Member] Committing transaction")
|
|
|
if safe_commit(conn):
|
|
|
print(f"[Add Member] Transaction committed successfully")
|
|
|
+ # 代数连续性校验
|
|
|
+ gen_warns = _check_member_generation(conn, member_id)
|
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
|
|
|
- return jsonify({"success": True, "message": "成员录入成功", "member_id": member_id})
|
|
|
+ return jsonify({"success": True, "message": "成员录入成功", "member_id": member_id,
|
|
|
+ "generation_warnings": gen_warns})
|
|
|
flash('成员录入成功')
|
|
|
+ for w in gen_warns:
|
|
|
+ flash(w, 'warning')
|
|
|
return redirect(url_for('members'))
|
|
|
else:
|
|
|
print(f"[Add Member] Transaction commit failed!")
|
|
|
@@ -3128,15 +3143,18 @@ def edit_member(member_id):
|
|
|
# 关系数据 - 支持多条关系
|
|
|
relations = []
|
|
|
i = 0
|
|
|
- while True:
|
|
|
+ while i < 50: # 最多 50 条,防止无限循环
|
|
|
parent_mid = request.form.get(f'relations[{i}][parent_mid]')
|
|
|
- rel_type = request.form.get(f'relations[{i}][relation_type]')
|
|
|
- sub_rel_type = request.form.get(f'relations[{i}][sub_relation_type]', '0')
|
|
|
- child_order_raw = request.form.get(f'relations[{i}][child_order]', '')
|
|
|
-
|
|
|
- if not parent_mid or not rel_type:
|
|
|
+ rel_type = request.form.get(f'relations[{i}][relation_type]')
|
|
|
+ # 该索引在表单中完全不存在时(非空值,而是键不存在)说明已超出行数,退出
|
|
|
+ if parent_mid is None and rel_type is None:
|
|
|
break
|
|
|
-
|
|
|
+ i += 1
|
|
|
+ # parent_mid 或 relation_type 为空(用户清除了该行)则跳过,不入库
|
|
|
+ if not parent_mid or not rel_type:
|
|
|
+ continue
|
|
|
+ sub_rel_type = request.form.get(f'relations[{i-1}][sub_relation_type]', '0')
|
|
|
+ child_order_raw = request.form.get(f'relations[{i-1}][child_order]', '')
|
|
|
child_order = int(child_order_raw) if child_order_raw.strip().isdigit() else None
|
|
|
relations.append({
|
|
|
'parent_mid': int(parent_mid),
|
|
|
@@ -3144,7 +3162,6 @@ def edit_member(member_id):
|
|
|
'sub_relation_type': int(sub_rel_type),
|
|
|
'child_order': child_order,
|
|
|
})
|
|
|
- i += 1
|
|
|
|
|
|
# For backward compatibility
|
|
|
if not relations:
|
|
|
@@ -3294,10 +3311,14 @@ def edit_member(member_id):
|
|
|
print(f"[Edit Member] Committing transaction")
|
|
|
conn.commit()
|
|
|
print(f"[Edit Member] Transaction committed successfully")
|
|
|
+ # 代数连续性校验
|
|
|
+ gen_warns = _check_member_generation(conn, member_id)
|
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.is_json:
|
|
|
- return jsonify({"success": True, "message": "成员信息更新成功"})
|
|
|
-
|
|
|
+ return jsonify({"success": True, "message": "成员信息更新成功",
|
|
|
+ "generation_warnings": gen_warns})
|
|
|
flash('成员信息更新成功')
|
|
|
+ for w in gen_warns:
|
|
|
+ flash(w, 'warning')
|
|
|
return redirect(url_for('members'))
|
|
|
|
|
|
with conn.cursor() as cursor:
|
|
|
@@ -6087,12 +6108,17 @@ def api_get_lineage(member_id):
|
|
|
break
|
|
|
visited_ancestor_ids.add(parent['id'])
|
|
|
|
|
|
- # 查祖父以获取该祖先的兄弟(优先亲生父母,排除养父)
|
|
|
- cursor.execute("""
|
|
|
+ # 查祖父以获取该祖先的兄弟
|
|
|
+ # 香火传承:优先养父(sub_type=3);血脉传承:优先生父
|
|
|
+ if mode == 'incense':
|
|
|
+ gp_order_wx = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
|
|
|
+ else:
|
|
|
+ gp_order_wx = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
|
|
|
+ cursor.execute(f"""
|
|
|
SELECT gp.id FROM family_relation_info r
|
|
|
JOIN family_member_info gp ON r.parent_mid = gp.id
|
|
|
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
|
|
|
+ ORDER BY {gp_order_wx}, r.id
|
|
|
LIMIT 1
|
|
|
""", (parent['id'],))
|
|
|
grandparent = cursor.fetchone()
|
|
|
@@ -6307,11 +6333,16 @@ def api_get_ancestors_above(ancestor_id):
|
|
|
break
|
|
|
visited_ids.add(parent['id'])
|
|
|
|
|
|
- cursor.execute("""
|
|
|
+ # 香火传承:优先养父(sub_type=3);血脉传承:优先生父
|
|
|
+ if mode == 'incense':
|
|
|
+ gp_order_wxab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 0 ELSE 1 END"
|
|
|
+ else:
|
|
|
+ gp_order_wxab = "CASE WHEN COALESCE(r.sub_relation_type, 0) = 3 THEN 1 ELSE 0 END"
|
|
|
+ cursor.execute(f"""
|
|
|
SELECT gp.id FROM family_relation_info r
|
|
|
JOIN family_member_info gp ON r.parent_mid = gp.id
|
|
|
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
|
|
|
+ ORDER BY {gp_order_wxab}, r.id
|
|
|
LIMIT 1
|
|
|
""", (parent['id'],))
|
|
|
grandparent = cursor.fetchone()
|
|
|
@@ -6775,5 +6806,218 @@ def mp_wx_update_member():
|
|
|
|
|
|
# ==================== End 微信小程序 API 接口 ====================
|
|
|
|
|
|
+# ==================== 代数连续性核查 ====================
|
|
|
+
|
|
|
+def _check_member_generation(conn, member_id):
|
|
|
+ """
|
|
|
+ 保存成员后调用:检查该成员与其亲生父亲的代数是否连续。
|
|
|
+ 返回 list of warning_str(空列表表示无问题)。
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cursor:
|
|
|
+ cursor.execute(
|
|
|
+ "SELECT name, simplified_name, name_word_generation FROM family_member_info WHERE id = %s",
|
|
|
+ (member_id,)
|
|
|
+ )
|
|
|
+ member = cursor.fetchone()
|
|
|
+ if not member:
|
|
|
+ return []
|
|
|
+
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT p.id, p.name, p.simplified_name, p.name_word_generation
|
|
|
+ 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 = 1
|
|
|
+ AND COALESCE(r.sub_relation_type, 0) != 3
|
|
|
+ LIMIT 5
|
|
|
+ """, (member_id,))
|
|
|
+ parents = cursor.fetchall()
|
|
|
+
|
|
|
+ if not parents:
|
|
|
+ return []
|
|
|
+
|
|
|
+ member_name = member.get('simplified_name') or member.get('name') or f'ID:{member_id}'
|
|
|
+ child_gens = _parse_generation_labels(member.get('name_word_generation') or '')
|
|
|
+
|
|
|
+ warnings = []
|
|
|
+ for parent in parents:
|
|
|
+ parent_gens = _parse_generation_labels(parent.get('name_word_generation') or '')
|
|
|
+ if not parent_gens or not child_gens:
|
|
|
+ continue
|
|
|
+ parent_map = dict(parent_gens)
|
|
|
+ child_map = dict(child_gens)
|
|
|
+ common = set(parent_map.keys()) & set(child_map.keys())
|
|
|
+ for prefix in common:
|
|
|
+ p_gen = parent_map[prefix]
|
|
|
+ c_gen = child_map[prefix]
|
|
|
+ if c_gen != p_gen + 1:
|
|
|
+ parent_name = parent.get('simplified_name') or parent.get('name') or '?'
|
|
|
+ expected = p_gen + 1
|
|
|
+ diff_desc = (
|
|
|
+ f"与父亲相同(均为第{c_gen}代)" if c_gen == p_gen
|
|
|
+ else f"跳跃了{c_gen - p_gen - 1}代(应为第{expected}代,实为第{c_gen}代)" if c_gen > p_gen + 1
|
|
|
+ else f"倒退(父第{p_gen}代,子第{c_gen}代)"
|
|
|
+ )
|
|
|
+ warnings.append(
|
|
|
+ f"⚠️ 代数不连续 [{prefix}]:{member_name} 第{c_gen}代,"
|
|
|
+ f"父亲 {parent_name} 第{p_gen}代,{diff_desc}"
|
|
|
+ )
|
|
|
+ return warnings
|
|
|
+ except Exception as e:
|
|
|
+ print(f"[GenerationCheck] Warning: {e}")
|
|
|
+ return []
|
|
|
+
|
|
|
+
|
|
|
+def _cn_to_int(s):
|
|
|
+ """将中文数字(如"三十二")转为整数,支持 1-999。"""
|
|
|
+ if not s:
|
|
|
+ return None
|
|
|
+ digits = {'零':0,'一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9}
|
|
|
+ units = {'十':10,'百':100,'千':1000}
|
|
|
+ result, temp = 0, 0
|
|
|
+ for ch in s:
|
|
|
+ if ch in digits:
|
|
|
+ temp = digits[ch]
|
|
|
+ elif ch in units:
|
|
|
+ if temp == 0 and ch == '十': # 十一 → 11,十 → 10
|
|
|
+ temp = 1
|
|
|
+ result += temp * units[ch]
|
|
|
+ temp = 0
|
|
|
+ result += temp
|
|
|
+ return result or None
|
|
|
+
|
|
|
+
|
|
|
+def _parse_generation_labels(gen_str):
|
|
|
+ """
|
|
|
+ 解析 name_word_generation 字段(可含多个分号分隔的代数标签),
|
|
|
+ 返回 list of (branch_prefix, gen_int)。
|
|
|
+ 例:"衢州第三十代;某支第五代" → [("衢州", 30), ("某支", 5)]
|
|
|
+ """
|
|
|
+ import re
|
|
|
+ if not gen_str:
|
|
|
+ return []
|
|
|
+ results = []
|
|
|
+ for part in gen_str.split(';'):
|
|
|
+ part = part.strip()
|
|
|
+ m = re.match(r'^(.*?)第([零一二三四五六七八九十百千]+)代$', part)
|
|
|
+ if m:
|
|
|
+ prefix = m.group(1)
|
|
|
+ num = _cn_to_int(m.group(2))
|
|
|
+ if num is not None:
|
|
|
+ results.append((prefix, num))
|
|
|
+ return results
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/manager/generation_check')
|
|
|
+def generation_check_page():
|
|
|
+ if 'user_id' not in session:
|
|
|
+ return redirect(url_for('login'))
|
|
|
+ return render_template('generation_check.html')
|
|
|
+
|
|
|
+
|
|
|
+@app.route('/manager/api/generation_check')
|
|
|
+def generation_check_api():
|
|
|
+ """
|
|
|
+ 核查所有父子关系中代数是否连续(子=父+1)。
|
|
|
+ 仅检查亲生父子关系(relation_type=1,排除入继 sub_type=3)。
|
|
|
+ 返回所有不连续的情况,包含人员信息方便排查。
|
|
|
+ """
|
|
|
+ if 'user_id' not in session:
|
|
|
+ return jsonify({"success": False, "message": "Unauthorized"}), 401
|
|
|
+
|
|
|
+ conn = get_db_connection()
|
|
|
+ try:
|
|
|
+ with conn.cursor() as cursor:
|
|
|
+ # 获取所有成员的代数字段
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT id, name, simplified_name, name_word_generation
|
|
|
+ FROM family_member_info
|
|
|
+ """)
|
|
|
+ members = {row['id']: row for row in cursor.fetchall()}
|
|
|
+
|
|
|
+ # 获取所有亲生父子关系(排除入继)
|
|
|
+ cursor.execute("""
|
|
|
+ SELECT parent_mid, child_mid
|
|
|
+ FROM family_relation_info
|
|
|
+ WHERE relation_type = 1
|
|
|
+ AND COALESCE(sub_relation_type, 0) != 3
|
|
|
+ """)
|
|
|
+ relations = cursor.fetchall()
|
|
|
+
|
|
|
+ errors = []
|
|
|
+ warnings = [] # 父或子缺少可解析代数
|
|
|
+
|
|
|
+ for rel in relations:
|
|
|
+ pid = rel['parent_mid']
|
|
|
+ cid = rel['child_mid']
|
|
|
+ parent = members.get(pid)
|
|
|
+ child = members.get(cid)
|
|
|
+ if not parent or not child:
|
|
|
+ continue
|
|
|
+
|
|
|
+ parent_gens = _parse_generation_labels(parent.get('name_word_generation') or '')
|
|
|
+ child_gens = _parse_generation_labels(child.get('name_word_generation') or '')
|
|
|
+
|
|
|
+ if not parent_gens or not child_gens:
|
|
|
+ # 至少一方没有可解析的代数,记为警告
|
|
|
+ if parent.get('name_word_generation') or child.get('name_word_generation'):
|
|
|
+ warnings.append({
|
|
|
+ 'type': 'missing_gen',
|
|
|
+ 'parent_id': pid,
|
|
|
+ 'parent_name': parent.get('simplified_name') or parent.get('name'),
|
|
|
+ 'parent_gen_raw': parent.get('name_word_generation') or '',
|
|
|
+ 'child_id': cid,
|
|
|
+ 'child_name': child.get('simplified_name') or child.get('name'),
|
|
|
+ 'child_gen_raw': child.get('name_word_generation') or '',
|
|
|
+ })
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 对每个共有的支系前缀检查是否 child_gen == parent_gen + 1
|
|
|
+ parent_map = dict(parent_gens)
|
|
|
+ child_map = dict(child_gens)
|
|
|
+ common_prefixes = set(parent_map.keys()) & set(child_map.keys())
|
|
|
+
|
|
|
+ if not common_prefixes:
|
|
|
+ # 前缀不同,可能是跨支系,不强制报错
|
|
|
+ continue
|
|
|
+
|
|
|
+ for prefix in sorted(common_prefixes):
|
|
|
+ p_gen = parent_map[prefix]
|
|
|
+ c_gen = child_map[prefix]
|
|
|
+ if c_gen != p_gen + 1:
|
|
|
+ errors.append({
|
|
|
+ 'type': 'discontinuous',
|
|
|
+ 'prefix': prefix,
|
|
|
+ 'parent_id': pid,
|
|
|
+ 'parent_name': parent.get('simplified_name') or parent.get('name'),
|
|
|
+ 'parent_gen': p_gen,
|
|
|
+ 'parent_gen_raw': parent.get('name_word_generation') or '',
|
|
|
+ 'child_id': cid,
|
|
|
+ 'child_name': child.get('simplified_name') or child.get('name'),
|
|
|
+ 'child_gen': c_gen,
|
|
|
+ 'child_gen_raw': child.get('name_word_generation') or '',
|
|
|
+ 'expected_child_gen': p_gen + 1,
|
|
|
+ 'diff': c_gen - p_gen,
|
|
|
+ })
|
|
|
+
|
|
|
+ # 按支系前缀和父代数排序
|
|
|
+ errors.sort(key=lambda x: (x['prefix'], x['parent_gen']))
|
|
|
+ warnings.sort(key=lambda x: (x['parent_id']))
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ "success": True,
|
|
|
+ "error_count": len(errors),
|
|
|
+ "warning_count": len(warnings),
|
|
|
+ "errors": errors,
|
|
|
+ "warnings": warnings,
|
|
|
+ })
|
|
|
+ except Exception as e:
|
|
|
+ import traceback
|
|
|
+ return jsonify({"success": False, "message": str(e), "trace": traceback.format_exc()}), 500
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+# ==================== End 代数连续性核查 ====================
|
|
|
+
|
|
|
if __name__ == '__main__':
|
|
|
app.run(debug=False, host='0.0.0.0', port=5001)
|