From afed4dfc3b1b79ba1beec26e68a1ba6ac1192c15 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期四, 17 九月 2026 14:39:59 +0800
Subject: [PATCH] fix(投资): 修复两级表头导致的取列错位 + 新增《弹窗提示》机制(14 条规则)

---
 docs/投资/工具/匹配与校验.py |  226 ++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 210 insertions(+), 16 deletions(-)

diff --git "a/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py" "b/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py"
index ab4734b..b57f5e6 100644
--- "a/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py"
+++ "b/docs/\346\212\225\350\265\204/\345\267\245\345\205\267/\345\214\271\351\205\215\344\270\216\346\240\241\351\252\214.py"
@@ -40,6 +40,8 @@
 }
 WANT = ['鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚', '寤鸿闃舵', '褰㈣薄杩涘害']
 NUMF = ['鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚']
+CITY17 = ('姝︽眽', '榛勭煶', '鍗佸牥', '瀹滄槍', '瑗勯槼', '閯傚窞', '鑽嗛棬', '瀛濇劅', '鑽嗗窞', '榛勫唸',
+          '鍜稿畞', '闅忓窞', '鎭╂柦', '浠欐', '娼滄睙', '澶╅棬', '绁炲啘鏋�')
 GROUP_PAT = re.compile(r'^(涓�|浜寍涓墊鍥泑浜攟鍏瓅涓億鍏�)銆�')
 SKIP_PAT = re.compile(r'^(灏廫s*璁鍚圽s*璁鎬籠s*璁�)$')
 
@@ -64,6 +66,13 @@
             s = s[:-2]
         s = s.rstrip('.銆�-')
     return s
+
+
+def normname2(s):
+    """鎶曡祫绯荤粺姣斿涓撶敤锛氬湪 normname 鍩虹涓婂啀鍘绘帀闂撮殧鍙风瓑绾楗板瓧绗︺��"""
+    for ch in ('路', '锛�', '鈥�', '鈻�', '鈼�', '.', '-'):
+        s = str(s or '').replace(ch, '')
+    return normname(s)
 
 
 def to_num(v):
@@ -144,6 +153,55 @@
     return out, period
 
 
+def normname3(s):
+    """鍐嶅幓鎺夋嫭鍙峰強鍏跺唴瀹癸紙鐢ㄤ簬璇嗗埆銆屽悕绉板浜嗘嫭娉ㄣ�嶇殑鍚屼竴椤圭洰锛夈��"""
+    t = normname(s)
+    t = re.sub(r'\([^)]*\)', '', t)
+    for ch in ('路', '锛�', '鈥�', '鈼�', '-', '.', '路'):
+        t = t.replace(ch, '')
+    return t
+
+
+def best_name_hint(key, tkeys, cutoff=0.80):
+    """鍦ㄧ洰鏍囪〃椤圭洰鍚嶉噷鎵句笌 key 鏈�鍍忕殑涓�涓紙瀹芥澗/鍘绘嫭娉ㄤ袱绉嶅彛寰勫彇鏈�浼橈級銆�
+
+    杩斿洖 (鐩爣琛ㄩ」鐩悕, 鐩镐技搴�)锛涙壘涓嶅埌杩斿洖 (None, 0)銆�
+    """
+    best = (None, 0.0)
+    for normf in (lambda s: normname(s, loose=True), normname3):
+        base = normf(key)
+        table = [normf(x) for x in tkeys]
+        hit = difflib.get_close_matches(base, table, n=1, cutoff=cutoff)
+        if hit:
+            ratio = difflib.SequenceMatcher(None, base, hit[0]).ratio()
+            if ratio > best[1]:
+                best = (tkeys[table.index(hit[0])], ratio)
+    return best
+
+
+def sys_lookup(key, table):
+    """鍦ㄦ姇璧勭郴缁熷瓧鍏搁噷鎵鹃」鐩細鍏堢簿纭紙鍚幓闂撮殧鍙凤級锛屽啀瀹芥澗+鐩镐技搴︼紙>=0.90锛夈��
+
+    杩斿洖 (璁板綍, 璇存槑)锛涙壘涓嶅埌杩斿洖 (None, '')銆�
+    """
+    if not table:
+        return None, ''
+    s = table.get(key)
+    if s:
+        return s, ''
+    for kk, vv in table.items():
+        if normname2(kk) == normname2(key):
+            return vv, '鍚嶇О鍐欐硶涓嶅悓锛堟姇璧勭郴缁燂細%s锛�' % vv['椤圭洰鍚嶇О']
+    idx = {}
+    for kk in table:
+        idx.setdefault(normname2(kk), []).append(kk)
+    cands = difflib.get_close_matches(normname2(key), list(idx.keys()), n=1, cutoff=0.90)
+    if cands:
+        kk = idx[cands[0]][0]
+        return table[kk], '鍚嶇О鍐欐硶涓嶅悓锛堟姇璧勭郴缁燂細%s锛�' % table[kk]['椤圭洰鍚嶇О']
+    return None, ''
+
+
 def find_system_files(indir, ym):
     """鍦ㄨ緭鍏ョ洰褰曢噷鎵俱�屾湰鏈�/涓婃湀銆嶆姇璧勭郴缁熸煡璇㈢粨鏋溿�傛枃浠跺悕鎴栥�庢椂鏈熴�忓垪閲岀殑骞存湀閮借銆�"""
     found = {}
@@ -195,6 +253,23 @@
     rep.append('> 鍙锛氭湰姝ラ涓嶆敼浠讳綍鏂囦欢锛涜緭鍑虹殑銆婂彉鏇存竻鍗曘�嬩緵銆屽啓鍏ヤ晶銆嶆墽琛屻��')
     rep.append('')
     changes = []
+    alerts = []
+
+    def add_alert(level, kind, cat, city, name, src, detail, action):
+        alerts.append(dict(绾у埆=level, 绫诲瀷=kind, 鍝佺被=cat, 甯傚窞=city, 椤圭洰鍚嶇О=name,
+                           鏉ユ簮=src, 璇︽儏=detail, 寤鸿鍔ㄤ綔=action))
+
+    # 婧愭枃浠惰В鏋愭牎楠岋紙鐢� 甯傚窞鏈堟姤瑙f瀽.py 浜у嚭锛�
+    checks = []
+    p_ck = os.path.join(out, '瑙f瀽鏍¢獙_%s.tsv' % a.month)
+    if os.path.exists(p_ck):
+        _lines = io.open(p_ck, encoding='utf-8-sig').read().splitlines()
+        if _lines:
+            _ckc = _lines[0].split('\t')
+            for _ln in _lines[1:]:
+                _v = _ln.split('\t')
+                _v += [''] * (len(_ckc) - len(_v))
+                checks.append(dict(zip(_ckc, _v)))
 
     # 鎶曡祫绯荤粺鏂囦欢锛堟湰鏈� / 涓婃湀锛�
     indir = os.path.join(a.root, '杈撳叆')
@@ -214,11 +289,41 @@
     rep.append('| 涓婃湀锛堟牎楠屽熀鏁帮級 | %s | %d |' % (os.path.basename(pre_path) if pre_path else '**鏈壘鍒帮紙鐢ㄦ埛鍙e緞 a锛氶渶姣忔湀鏀句竴浠斤級**', len(cur_pre)))
     rep.append('')
 
+    # 鈥斺�� 寮圭獥瑙勫垯锛氭姇璧勭郴缁熸枃浠剁己澶� / 婧愭枃浠惰В鏋愬紓甯� / 婧愭枃浠跺唴閮ㄥ涓嶄笂 鈥斺��
+    if not cur_path:
+        add_alert('闃绘柇', '缂烘湰鏈熸姇璧勭郴缁熻〃', '', '', '', os.path.basename(indir),
+                  '鏈湪銆岃緭鍏ャ�嶇洰褰曟壘鍒� %s 鐨勬姇璧勭郴缁熸煡璇㈢粨鏋�' % a.month,
+                  '鎶婃湰鏈熸姇璧勭郴缁熷鍑虹殑銆婃ā鏉縚鏌ヨ缁撴灉銆嬫斁杩� docs\\鎶曡祫\\杈撳叆\\ 鍚庨噸璺�')
+    if not pre_path:
+        add_alert('璀﹀憡', '缂轰笂鏈堟姇璧勭郴缁熻〃', '', '', '', os.path.basename(indir),
+                  '鏈壘鍒� %s 鐨勬姇璧勭郴缁熸煡璇㈢粨鏋� 鈫� 鏈堝害鏍¢獙锛堟湰鏈堣嚜骞村垵绱 = 涓婃湀鑷勾鍒濈疮璁� + 鏈湀瀹屾垚锛夋棤娉曟墽琛�' % prev_ym(a.month),
+                  '姣忔湀鎶婁笂鏈堛�佹湰鏈堜袱浠芥姇璧勭郴缁熸煡璇㈢粨鏋滈兘鏀捐繘 docs\\鎶曡祫\\杈撳叆\\')
+    if not checks:
+        add_alert('闃绘柇', '缂烘簮鏂囦欢瑙f瀽鏍¢獙', '', '', '', os.path.basename(p_ck),
+                  '鏈壘鍒般�婅В鏋愭牎楠屻�嬫竻鍗曪紝鏃犳硶鍒ゆ柇婧愭枃浠舵槸鍚﹁嚜娲姐�佹槸鍚﹁В鏋愬け璐�',
+                  '鍏堣窇 甯傚窞鏈堟姤瑙f瀽.py --month %s' % a.month)
+    for ck in checks:
+        st = ck.get('鐘舵��', 'OK')
+        if st and st != 'OK':
+            add_alert('闃绘柇' if st.startswith('瑙f瀽澶辫触') else '璀﹀憡', '婧愭枃浠惰В鏋愬紓甯�',
+                      ck.get('鍝佺被', ''), ck.get('甯傚窞', ''), '', ck.get('鏂囦欢', ''),
+                      st, '浜哄伐鎵撳紑婧愭枃浠剁‘璁よ〃澶�/鏁版嵁锛涘繀瑕佹椂鐧昏鐗逛緥鍚庡啀瀵煎叆')
+        elif ck.get('涓�鑷存��', '') == '鈿狅笍 涓嶄竴鑷�':
+            add_alert('璀﹀憡', '婧愭枃浠跺唴閮ㄥ涓嶄笂', ck.get('鍝佺被', ''), ck.get('甯傚窞', ''), '', ck.get('鏂囦欢', ''),
+                      '椤圭洰琛屾眰鍜� %s/%s/%s 鈮� %s 琛屽悎璁� %s/%s/%s' % (
+                          ck.get('姹傚拰_鑷紑濮嬪缓璁剧疮璁�', ''), ck.get('姹傚拰_鑷勾鍒濈疮璁�', ''), ck.get('姹傚拰_鏈湀瀹屾垚', ''),
+                          ck.get('鍚堣琛�', ''), ck.get('鍚堣_鑷紑濮嬪缓璁剧疮璁�', ''),
+                          ck.get('鍚堣_鑷勾鍒濈疮璁�', ''), ck.get('鍚堣_鏈湀瀹屾垚', '')),
+                      '璁╁競宸炴牳瀵规簮鏂囦欢锛堝悎璁¤鎴栭」鐩鏈夐敊锛夛紝纭鍚庡啀瀵煎叆')
+
     for cat, spec in TARGETS.items():
         rep.append('## %s 鈥斺�� 鐩爣鏄庣粏琛�' % cat)
         rep.append('')
         if not os.path.exists(spec['path']):
-            rep.append('鐩爣琛ㄤ笉瀛樺湪锛�%s' % spec['path']); rep.append(''); continue
+            rep.append('鐩爣琛ㄤ笉瀛樺湪锛�%s' % spec['path']); rep.append('')
+            add_alert('闃绘柇', '缂虹洰鏍囨槑缁嗚〃', cat, '', '', spec['path'],
+                      '鐩爣鏄庣粏琛ㄤ笉瀛樺湪锛屾湰瀛e害璇ュ搧绫绘棤娉曞啓鍏�', '鎶婃ā鏉胯〃鏀惧埌 docs\\鎶曡祫\\杈撳嚭\\')
+            continue
         tgt = read_target(spec)
         prow = [r for r in parsed if r['鍝佺被'] == cat]
         tmap = {}
@@ -235,12 +340,19 @@
         loose_tgt = {}
         for k in tmap:
             loose_tgt.setdefault(normname(k, loose=True), []).append(k)
+        plain_tgt = {}
+        for k in tmap:
+            plain_tgt.setdefault(normname3(k), []).append(k)
         tkeys = list(tmap.keys())
         new_in_src, suspicious = [], []
         for k in raw_new:
             nl = normname(k, loose=True)
             if nl in loose_tgt:
                 suspicious.append((k, loose_tgt[nl][0], '瀹芥澗绛変环锛堝幓鎷彿/鍘烩�滈」鐩�濅簩瀛楀悗鐩稿悓锛�'))
+                continue
+            n3 = normname3(k)
+            if n3 and n3 in plain_tgt:
+                suspicious.append((k, plain_tgt[n3][0], '鍘绘帀鎷敞涓庨棿闅斿彿鍚庣浉鍚�'))
                 continue
             close = difflib.get_close_matches(nl, [normname(x, loose=True) for x in tkeys], n=1, cutoff=0.90)
             if close:
@@ -254,6 +366,12 @@
         rep.append('- **鐤戜技鍚屼竴椤圭洰銆侀渶浜哄伐纭** **%d**锛堟寜鐢ㄦ埛鍙e緞锛�**涓嶈嚜鍔ㄦ柊澧炪�佷笉鍔ㄥ悕绉帮紝寮圭獥鎻愮ず**锛�' % len(suspicious))
         rep.append('- 鐩爣琛ㄦ湁浣嗘湰鏈熸簮鏂囦欢娌℃湁锛堜繚鎸佸師鍊硷級**%d**' % len(only_tgt))
         rep.append('')
+        for k1, hit1, why1 in suspicious:
+            _p1 = pmap[k1][0]
+            add_alert('璀﹀憡', '鐤戜技鍚屼竴椤圭洰锛堝悕绉板啓娉曚笉鍚岋級', cat, _p1.get('甯傚窞', ''), _p1['椤圭洰鍚嶇О'],
+                      os.path.basename(_p1['鏂囦欢']),
+                      '涓庣洰鏍囪〃宸叉湁椤圭洰銆�%s銆�%s' % (tmap[hit1][0]['椤圭洰鍚嶇О'], why1),
+                      '浜哄伐纭鏄惁鍚屼竴椤圭洰锛氭槸 鈫� 涓嶆敼鍚嶇О銆佹寜鎶曡祫绯荤粺鍚嶇О鏇存柊锛涘惁 鈫� 鏁磋鏂板')
         if suspicious:
             rep.append('### 鈿狅笍 鐤戜技鍚屼竴椤圭洰浣嗗悕绉板啓娉曚笉鍚� 鈫� 闇�浜哄伐纭锛堢▼搴忎笉鑷姩鍚堝苟銆佷篃涓嶉噸澶嶆柊澧烇級')
             rep.append('')
@@ -267,16 +385,17 @@
             rep.append('')
             rep.append('| 椤圭洰鍚嶇О | 鏉ユ簮鏂囦欢 | 鑷紑濮嬪缓璁剧疮璁� | 鑷勾鍒濈疮璁� | 鏈湀瀹屾垚 | 鍚嶇О鐩镐技搴︽彁绀� |')
             rep.append('| --- | --- | --- | --- | --- | --- |')
-            loose_keys = [normname(x, loose=True) for x in tkeys]
             for k in new_in_src:
                 p0 = pmap[k][0]
-                nl = normname(k, loose=True)
-                close = difflib.get_close_matches(nl, loose_keys, n=1, cutoff=0.80)
-                hint = ''
-                if close:
-                    hit = tkeys[loose_keys.index(close[0])]
-                    hint = '鈿狅笍 涓庛��%s銆嶇浉浼� %.2f锛岃纭鏄惁鍚屼竴椤圭洰' % (
-                        hit, difflib.SequenceMatcher(None, nl, close[0]).ratio())
+                _hit, _r = best_name_hint(k, tkeys)
+                hint = ('鈿狅笍 涓庛��%s銆嶇浉浼� %.2f锛岃纭鏄惁鍚屼竴椤圭洰' % (_hit, _r)) if _hit else ''
+                _sim = hint
+                add_alert('鎻愮ず', '灏嗘柊澧為」鐩�', cat, p0.get('甯傚窞', ''), p0['椤圭洰鍚嶇О'],
+                          os.path.basename(p0['鏂囦欢']),
+                          '鐩爣琛ㄦ病鏈夎繖涓」鐩紱鑷紑濮嬪缓璁剧疮璁�/鑷勾鍒濈疮璁�/鏈湀瀹屾垚 = %s/%s/%s%s' % (
+                              p0.get('鑷紑濮嬪缓璁剧疮璁�', ''), p0.get('鑷勾鍒濈疮璁�', ''), p0.get('鏈湀瀹屾垚', ''),
+                              ('锛�' + hint) if hint else ''),
+                          '纭鏄湡瀹炴柊椤圭洰鍚庡啓鍏ワ紙鏁磋鎻掑叆锛屽鍒剁浉閭昏鏍煎紡锛�')
                 rep.append('| %s | %s | %s | %s | %s | %s |' % (
                     p0['椤圭洰鍚嶇О'], os.path.basename(p0['鏂囦欢'])[:30], p0.get('鑷紑濮嬪缓璁剧疮璁�', ''),
                     p0.get('鑷勾鍒濈疮璁�', ''), p0.get('鏈湀瀹屾垚', ''), hint))
@@ -295,23 +414,51 @@
                                 '椤圭洰鍚嶇О': p['椤圭洰鍚嶇О'], '鍐欏叆鏉ユ簮': os.path.basename(p['鏂囦欢']),
                                 **{c: p.get(c, '') for c in WANT}})
 
+        # 鐩爣琛ㄦ湁銆佹湰鏈熸簮鏂囦欢娌℃湁 鈫� 淇濇寔鍘熷�硷紙鎻愮ず锛屼笉闃绘柇锛�
+        if only_tgt:
+            _names = [tmap[k][0]['椤圭洰鍚嶇О'] for k in only_tgt]
+            add_alert('鎻愮ず', '鏈湡婧愭枃浠堕噷娌℃湁鐨勯」鐩�', cat, '', '', '鈥�',
+                      '鐩爣琛ㄩ噷鏈� %d 涓」鐩湰鏈熷競宸炴湀鎶ユ病鎶ワ細%s%s' % (
+                          len(_names), '銆�'.join(_names[:8]), ' 绛�' if len(_names) > 8 else ''),
+                      '鎸夊彛寰勪繚鎸佷笂鏈堝師鍊硷紱鑻ュ競宸炵‘瀹炲凡鍋滃缓锛岃浜哄伐纭鏄惁淇濈暀')
+
         # 涓夋柟鏁板�兼瘮瀵癸細婧愭枃浠� vs 鎶曡祫绯荤粺锛堟湰鏈燂級
         if cur_sys:
-            diff, missing = [], []
+            diff, missing, shifted = [], [], []
             for k in matched:
                 p, t = pmap[k][0], tmap[k][0]
-                s = cur_sys.get(k)
+                s, note = sys_lookup(k, cur_sys)
                 if not s:
                     missing.append(t['椤圭洰鍚嶇О'])
                     continue
-                for c in ('鑷紑濮嬪缓璁剧疮璁�', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
+                if note:
+                    add_alert('鎻愮ず', '鎶曡祫绯荤粺鍚嶇О鍐欐硶涓嶅悓', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                              os.path.basename(p['鏂囦欢']), note + '锛涘凡鎸夋姇璧勭郴缁熻繖鏉¤褰曞仛鏁板�煎鐓э紙椤圭洰鍚嶇О涓嶅姩锛�',
+                              '鐭ユ倝鍗冲彲')
+                for c in ('鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
                     pv, sv = to_num(p.get(c)), s.get(c)
                     if pv is None or sv is None:
                         continue
-                    if abs(pv - sv) > 0.05:
-                        diff.append((t['椤圭洰鍚嶇О'], c, pretty(pv), pretty(sv), pretty(sv - pv)))
+                    if abs(pv - sv) <= 0.05:
+                        continue
+                    diff.append((t['椤圭洰鍚嶇О'], c, pretty(pv), pretty(sv), pretty(sv - pv)))
+                    if abs(pv - sv) > 1.0:
+                        add_alert('璀﹀憡', '婧愭枃浠朵笌鎶曡祫绯荤粺涓嶄竴鑷�', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                                  os.path.basename(p['鏂囦欢']),
+                                  '%s锛氬競宸炴湀鎶� %s 锛� 鎶曡祫绯荤粺 %s锛堝樊 %s锛�' % (c, pretty(pv), pretty(sv), pretty(sv - pv)),
+                                  '浜哄伐纭浠ュ摢涓负鍑嗭紱鑻ユ槸鍙栧垪閿欎綅鍒欎慨瑙f瀽瑙勫垯鍚庨噸璺�')
+                    for c2 in ('鎬绘姇璧�', '鑷紑濮嬪缓璁剧疮璁�', '鏈勾璁″垝鎶曡祫', '鑷勾鍒濈疮璁�', '鏈湀瀹屾垚'):
+                        if c2 == c:
+                            continue
+                        sv2 = s.get(c2)
+                        if sv2 is not None and abs(pv - sv2) <= 0.05:
+                            shifted.append((t['椤圭洰鍚嶇О'], c, c2))
+                            add_alert('璀﹀憡', '鐤戜技鍙栧垪閿欎綅', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                                      os.path.basename(p['鏂囦欢']),
+                                      '甯傚窞鏈堟姤銆�%s銆�= %s锛屾伆濂界瓑浜庢姇璧勭郴缁熴��%s銆�' % (c, pretty(pv), c2),
+                                      '妫�鏌ユ簮鏂囦欢琛ㄥご锛堢粍琛ㄥご鍚堝苟鍗曞厓鏍兼渶瀹规槗閿欎綅锛夛紝淇彇鍒楄鍒欏悗閲嶈窇瑙f瀽')
             rep.append('')
-            rep.append('### 婧愭枃浠� vs 鎶曡祫绯荤粺锛堟湰鏈燂級锛�**鏁板�间笉涓�鑷� %d 澶�**锛堥渶寮圭獥鎻愮ず锛�' % len(diff))
+            rep.append('### 婧愭枃浠� vs 鎶曡祫绯荤粺锛堟湰鏈燂級锛�**鏁板�间笉涓�鑷� %d 澶�**锛堥渶寮圭獥鎻愮ず锛夛紝鍏朵腑鐤戜技鍙栧垪閿欎綅 **%d** 澶�' % (len(diff), len(shifted)))
             rep.append('')
             if diff:
                 rep.append('| 椤圭洰鍚嶇О | 瀛楁 | 甯傚窞鏈堟姤 | 鎶曡祫绯荤粺 | 宸 |')
@@ -326,6 +473,9 @@
             for nm1 in missing[:60]:
                 rep.append('- %s' % nm1)
             rep.append('')
+            for nm1 in missing:
+                add_alert('鎻愮ず', '鎶曡祫绯荤粺鏌ユ棤姝ら」鐩�', cat, '', nm1, '鈥�',
+                          '鎶曡祫绯荤粺鏈湡鏌ヨ缁撴灉閲屾病鏈夎繖涓」鐩悕绉�', '鐭ユ倝鍗冲彲锛涘闇�浠ユ姇璧勭郴缁熶负鍑嗭紝璇蜂汉宸ユ牳瀵瑰悕绉�')
 
         # 鏈堝害鏍¢獙锛氭湰鏈堣嚜骞村垵绱 = 涓婃湀鑷勾鍒濈疮璁� + 鏈湀瀹屾垚
         if cur_pre:
@@ -334,7 +484,7 @@
                 p, t = pmap[k][0], tmap[k][0]
                 nowj = to_num(p.get('鑷勾鍒濈疮璁�'))
                 nowk = to_num(p.get('鏈湀瀹屾垚'))
-                pre = cur_pre.get(k)
+                pre, _note = sys_lookup(k, cur_pre)
                 if nowj is None or nowk is None or not pre:
                     continue
                 prej = pre.get('鑷勾鍒濈疮璁�')
@@ -342,6 +492,11 @@
                     continue
                 if abs(nowj - (prej + nowk)) > 0.05:
                     bad.append((t['椤圭洰鍚嶇О'], pretty(prej), pretty(nowk), pretty(nowj), pretty(prej + nowk)))
+                    add_alert('璀﹀憡', '鏈堝害绛夊紡涓嶇', cat, p.get('甯傚窞', ''), t['椤圭洰鍚嶇О'],
+                              '%s + %s = %s锛岃�屾湰鏈堣嚜骞村垵绱鏄� %s锛堝樊 %s锛�' % (
+                                  pretty(prej), pretty(nowk), pretty(prej + nowk), pretty(nowj),
+                                  pretty(nowj - prej - nowk)),
+                              '鍐欏叆鏃舵寜绛夊紡绾犳涓� %s锛屽苟璁╁競宸炵‘璁�' % pretty(prej + nowk))
             rep.append('### 鏈堝害鏍¢獙銆屾湰鏈堣嚜骞村垵绱 = 涓婃湀鑷勾鍒濈疮璁� + 鏈湀瀹屾垚銆嶏細涓嶇 **%d** 椤�' % len(bad))
             rep.append('')
             if bad:
@@ -354,6 +509,43 @@
             rep.append('### 鏈堝害鏍¢獙锛�**璺宠繃**锛堟湭鎵惧埌涓婃湀鎶曡祫绯荤粺鏁版嵁琛紝鎸夌敤鎴峰彛寰� (a) 闇�姣忔湀鏀句竴浠斤級')
             rep.append('')
 
+    # 鏈湡娌℃湁浠讳綍椤圭洰琛岀殑甯傚窞 鈫� 鎻愮ず锛堝彲鑳芥紡鎶ワ級
+    for cat in TARGETS:
+        _cities = {r['甯傚窞'] for r in parsed if r['鍝佺被'] == cat and r['甯傚窞']}
+        _miss = [c for c in CITY17 if c not in _cities]
+        if _miss:
+            add_alert('鎻愮ず', '鏈湡鏃犺甯傚窞鏁版嵁', cat, '', '', '鈥�',
+                      '鍏� %d 涓競宸炴湰鏈熸病鏈変换浣曢」鐩锛�%s' % (len(_miss), '銆�'.join(_miss)),
+                      '纭鏄�岃甯傚窞纭疄娌℃湁椤圭洰銆嶈繕鏄�屾紡鎶ャ�嶏紱婕忔姤鍒欒甯傚窞琛ユ姤')
+
+    # ---- 姹囨�汇�婂脊绐楁彁绀恒�嬶紙鏈潵鎼繘 Java 鍚庣鏃舵寜姝ら�愭潯寮圭獥锛� ----
+    _lv = {'闃绘柇': 0, '璀﹀憡': 1, '鎻愮ず': 2}
+    alerts.sort(key=lambda x: (_lv.get(x['绾у埆'], 9), x['绫诲瀷'], x['鍝佺被'], x['甯傚窞'], x['椤圭洰鍚嶇О']))
+    _n = {k: sum(1 for x in alerts if x['绾у埆'] == k) for k in ('闃绘柇', '璀﹀憡', '鎻愮ず')}
+    a_cols = ['搴忓彿', '绾у埆', '绫诲瀷', '鍝佺被', '甯傚窞', '椤圭洰鍚嶇О', '鏉ユ簮', '璇︽儏', '寤鸿鍔ㄤ綔']
+    p_al = os.path.join(out, '寮圭獥鎻愮ず_%s.tsv' % a.month)
+    with io.open(p_al, 'w', encoding='utf-8-sig') as fh:
+        fh.write('\t'.join(a_cols) + '\n')
+        for _i, _x in enumerate(alerts, 1):
+            fh.write('\t'.join(str(_i) if _k == '搴忓彿' else str(_x.get(_k, '')).replace('\t', ' ')
+                                for _k in a_cols) + '\n')
+    al_md = ['## 鈿狅笍 寮圭獥鎻愮ず鎬昏锛堝叡 %d 椤癸細闃绘柇 %d 锛� 璀﹀憡 %d 锛� 鎻愮ず %d锛�' % (
+                 len(alerts), _n['闃绘柇'], _n['璀﹀憡'], _n['鎻愮ず']), '']
+    al_md.append('> 绾у埆锛�**闃绘柇**=涓嶈兘鍐欒〃锛屽厛澶勭悊锛�**璀﹀憡**=鏁版嵁鏈夌枒鐐癸紝浜哄伐纭鍚庡啀鍐欙紱**鎻愮ず**=浠呭憡鐭ャ��')
+    al_md.append('> 鏈哄櫒鍙娓呭崟锛歚%s`锛堟湭鏉� Java/缃戦〉鎸夋閫愭潯寮圭獥锛夈�傝鍒欒 `docs\\鎶曡祫\\寮圭獥鎻愮ず瑙勫垯_2026-09-17.md`銆�'
+                 % os.path.basename(p_al))
+    al_md.append('')
+    if alerts:
+        al_md.append('| 搴忓彿 | 绾у埆 | 绫诲瀷 | 鍝佺被 | 甯傚窞 | 椤圭洰鍚嶇О | 璇︽儏 | 寤鸿鍔ㄤ綔 |')
+        al_md.append('| --- | --- | --- | --- | --- | --- | --- | --- |')
+        for _i, _x in enumerate(alerts, 1):
+            al_md.append('| %d | %s | %s | %s | %s | %s | %s | %s |' % (
+                _i, _x['绾у埆'], _x['绫诲瀷'], _x['鍝佺被'], _x['甯傚窞'], _x['椤圭洰鍚嶇О'], _x['璇︽儏'], _x['寤鸿鍔ㄤ綔']))
+        al_md.append('')
+    else:
+        al_md.append('锛堟棤锛氭墍鏈夋鏌ラ」鍧囬�氳繃锛�'); al_md.append('')
+    rep = rep[:4] + al_md + rep[4:]
+
     cols = ['鍝佺被', '鍔ㄤ綔', '鐩爣琛屽彿', '椤圭洰鍚嶇О', '鍐欏叆鏉ユ簮'] + WANT
     p_tsv = os.path.join(out, '鍙樻洿娓呭崟_%s.tsv' % a.month)
     with io.open(p_tsv, 'w', encoding='utf-8-sig') as fh:
@@ -363,8 +555,10 @@
     p_md = os.path.join(out, '鍖归厤鏍¢獙鎶ュ憡_%s.md' % a.month)
     io.open(p_md, 'w', encoding='utf-8').write('\n'.join(rep) + '\n')
     print('鍙樻洿鏉℃暟 = %d' % len(changes))
+    print('寮圭獥鎻愮ず = %d锛堥樆鏂� %d / 璀﹀憡 %d / 鎻愮ず %d锛�' % (len(alerts), _n['闃绘柇'], _n['璀﹀憡'], _n['鎻愮ず']))
     print('wrote', p_md)
     print('wrote', p_tsv)
+    print('wrote', p_al)
 
 
 if __name__ == '__main__':

--
Gitblit v1.9.1