Browse Source

feta#飞书正文样式优化-错误红色加粗.

hefuzheng 1 tuần trước cách đây
mục cha
commit
91db3ef9d6

BIN
code/post_clear_check/src/post_clear_check/__pycache__/dimension_body.cpython-312.pyc


BIN
code/post_clear_check/src/post_clear_check/__pycache__/feishu.cpython-312.pyc


+ 64 - 47
code/post_clear_check/src/post_clear_check/dimension_body.py

@@ -22,7 +22,7 @@ from post_clear_check.chain import (
     INDEX_MARK,
     STATUS_MARK,
 )
-from post_clear_check.feishu import FeishuError, card_text
+from post_clear_check.feishu import FeishuError, card_text, emphasize_verdict_block
 from post_clear_check.gate import (
     FORMULA_DAYCUT,
     FORMULA_EARLY_REPORT,
@@ -356,14 +356,17 @@ def _field_blocks(rows: Sequence[_View]) -> list[str]:
         actual = f"{shown} 无差异" if verdict == VERDICT_CORRECT else _actual(sample)
         label = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
         blocks.append(
-            _lines(
-                (
-                    f"字段:{_clear_title(formula)}",
-                    f"期望:{_expect(sample) if verdict != VERDICT_CORRECT else formula}",
-                    f"实据:{actual}",
-                    f"依据:{basis}",
-                    f"结论:{label}({correct}/{len(group)})",
-                )
+            emphasize_verdict_block(
+                _lines(
+                    (
+                        f"字段:{_clear_title(formula)}",
+                        f"期望:{_expect(sample) if verdict != VERDICT_CORRECT else formula}",
+                        f"实据:{actual}",
+                        f"依据:{basis}",
+                        f"结论:{label}({correct}/{len(group)})",
+                    )
+                ),
+                verdict,
             )
         )
     return blocks
@@ -436,15 +439,18 @@ def _report_block(rows: Sequence[_View], business_date: date) -> str:
     bases = _unique(view.basis for view in rows if view.basis)
     label_text = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
     actual = f"{len(rows)} 列无差异" if verdict == VERDICT_CORRECT else f"{correct}/{len(rows)} 列无差异,其余见下方清单"
-    return _lines(
-        (
-            f"报表:{label}",
-            f"筛选:{screen}",
-            _expect_line(names),
-            f"实据:{actual}",
-            _basis_line(bases or ["见各列登记"]),
-            f"结论:{label_text}",
-        )
+    return emphasize_verdict_block(
+        _lines(
+            (
+                f"报表:{label}",
+                f"筛选:{screen}",
+                _expect_line(names),
+                f"实据:{actual}",
+                _basis_line(bases or ["见各列登记"]),
+                f"结论:{label_text}",
+            )
+        ),
+        verdict,
     )
 
 
@@ -462,15 +468,18 @@ def _step_block(formula: str, rows: Sequence[_View], business_date: date) -> str
     bases = _unique(view.basis for view in rows if view.basis)
     label_text = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
     actual = f"{len(rows)} 条无差异" if verdict == VERDICT_CORRECT else f"{correct}/{len(rows)} 条无差异,其余见下方清单"
-    return _lines(
-        (
-            f"步骤:{_step_name(formula)}",
-            f"筛选:{screen}",
-            _expect_line([formula]),
-            f"实据:{actual}",
-            _basis_line(bases or ["见各列登记"]),
-            f"结论:{label_text}",
-        )
+    return emphasize_verdict_block(
+        _lines(
+            (
+                f"步骤:{_step_name(formula)}",
+                f"筛选:{screen}",
+                _expect_line([formula]),
+                f"实据:{actual}",
+                _basis_line(bases or ["见各列登记"]),
+                f"结论:{label_text}",
+            )
+        ),
+        verdict,
     )
 
 
@@ -567,18 +576,22 @@ def _detail(view: _View) -> str:
     if view.basis:
         rows.append(f"依据:{view.basis}")
     rows.append(_conclusion(view))
-    return _lines(rows)
+    # 错误清单红加粗;可疑清单与无法展开同款黄标签结论。
+    return emphasize_verdict_block(_lines(rows), view.verdict)
 
 
 def _gate_block(view: _View) -> str:
-    return _lines(
-        (
-            f"字段:{view.formula_text}",
-            f"期望:{view.check_text or view.formula_text}",
-            f"实据:{view.calc_text or '当前缺失'}",
-            f"依据:{view.basis}",
-            _conclusion(view),
-        )
+    return emphasize_verdict_block(
+        _lines(
+            (
+                f"字段:{view.formula_text}",
+                f"期望:{view.check_text or view.formula_text}",
+                f"实据:{view.calc_text or '当前缺失'}",
+                f"依据:{view.basis}",
+                _conclusion(view),
+            )
+        ),
+        view.verdict,
     )
 
 
@@ -712,19 +725,23 @@ def _index_block(text: str) -> str:
     detail = "对应报表卡"
     if "明细在" in text:
         detail = "明细在" + text.split("明细在", 1)[1].strip("。")
-    verdict = "正确"
+    verdict = VERDICT_CORRECT
     if "错误" in text:
-        verdict = "错误"
+        verdict = VERDICT_ERROR
     elif "可疑" in text:
-        verdict = "可疑"
-    return _lines(
-        (
-            f"字段:{head or text}",
-            "期望:本轮主单在该报表上的列与对应报表卡一致",
-            f"实据:{actual or text}",
-            f"依据:{detail}",
-            f"结论:{verdict}",
-        )
+        verdict = VERDICT_SUSPECT
+    label = {VERDICT_CORRECT: "正确", VERDICT_ERROR: "错误", VERDICT_SUSPECT: "可疑"}[verdict]
+    return emphasize_verdict_block(
+        _lines(
+            (
+                f"字段:{head or text}",
+                "期望:本轮主单在该报表上的列与对应报表卡一致",
+                f"实据:{actual or text}",
+                f"依据:{detail}",
+                f"结论:{label}",
+            )
+        ),
+        verdict,
     )
 
 

+ 41 - 2
code/post_clear_check/src/post_clear_check/feishu.py

@@ -223,6 +223,44 @@ def card_text(value: str) -> str:
     return cleaned.replace("<", "<").replace(">", ">")
 
 
+def _md_plain(text: str) -> str:
+    """包进 **加粗** 前,只把星号改成实体,避免撕开标记。
+
+    下划线不转义:业务正文常见 after_val、fund_account 等标识,且加粗用的是
+    `**` 不是 `__`,单独的 `_` 一般不会拆开加粗。
+    """
+
+    return text.replace("*", "&#42;")
+
+
+def emphasize_verdict_block(text: str, verdict: str) -> str:
+    """错误整段红加粗;可疑(含无法展开)整段加粗,结论行用黄标签。
+
+    必须在 card_text 之后调用:这里留下的 `<font>` / `<text_tag>` 是版式标签,
+    再进 card_text 会被改成全角而失效。正确块原样返回。
+    """
+
+    if not text or verdict == VERDICT_CORRECT:
+        return text
+    if verdict not in (VERDICT_ERROR, VERDICT_SUSPECT):
+        return text
+    styled: list[str] = []
+    for line in text.split("\n"):
+        if not line:
+            styled.append(line)
+            continue
+        plain = _md_plain(line)
+        # 可疑结论整行进黄标签;其余行加粗。无法展开的结论也以「结论:可疑」开头。
+        if verdict == VERDICT_SUSPECT and line.startswith("结论:可疑"):
+            styled.append(f"<text_tag color='yellow'>{plain}</text_tag>")
+        else:
+            styled.append(f"**{plain}**")
+    body = "\n".join(styled)
+    if verdict == VERDICT_ERROR:
+        return f"<font color='red'>{body}</font>"
+    return body
+
+
 def render_title(
     verdict: str,
     chain_name: str,
@@ -596,7 +634,8 @@ def _detail_block(line: ConclusionView, chain_name: str) -> str:
     if basis:
         rows.append(f"依据:{basis}")
     rows.append(_conclusion(line, notes))
-    return "\n\n".join(rows)
+    # 错误、可疑、无法展开整段醒目;不适用仍是正确结论,不套色。
+    return emphasize_verdict_block("\n\n".join(rows), line.verdict)
 
 
 def _correct_groups(lines: Sequence[ConclusionView]) -> tuple[_CorrectGroup, ...]:
@@ -776,7 +815,7 @@ def _order_field_block(line: ConclusionView) -> str:
     if basis:
         rows.append(f"依据:{basis}")
     rows.append(_order_conclusion(line, notes))
-    return "\n\n".join(rows)
+    return emphasize_verdict_block("\n\n".join(rows), line.verdict)
 
 
 def _link_sentence(line: ConclusionView) -> str:

BIN
code/post_clear_check/tests/__pycache__/test_feishu.cpython-312-pytest-9.1.1.pyc


+ 48 - 8
code/post_clear_check/tests/test_feishu.py

@@ -40,6 +40,7 @@ from post_clear_check.feishu import (
     build_payload,
     card_verdict,
     classify_response,
+    emphasize_verdict_block,
     expectation,
     pack_bodies,
     render_body,
@@ -763,11 +764,13 @@ def test_error_theme_posts_only_error_rows() -> None:
     assert "\n\n盘中公式\n\n" not in text
     assert (
         "关联报表\n\n"
-        "字段:错误公式\n\n"
-        "期望:3 + 1 = 4\n\n"
-        "实据:报表.乙 = 9。当前 9.00\n\n"
-        "依据:依据\n\n"
-        "结论:错误。不符合预期。这是错误说明。"
+        "<font color='red'>"
+        "**字段:错误公式**\n\n"
+        "**期望:3 + 1 = 4**\n\n"
+        "**实据:报表.乙 = 9。当前 9.00**\n\n"
+        "**依据:依据**\n\n"
+        "**结论:错误。不符合预期。这是错误说明。**"
+        "</font>"
     ) in text
 
 
@@ -1012,7 +1015,7 @@ def test_sample_line_is_the_first_body_line() -> None:
 
 
 def test_body_strips_control_characters_and_angle_brackets() -> None:
-    """制表符去掉,尖括号改成全角,避免 lark_md 当成标签。"""
+    """制表符去掉;业务尖括号改成全角。错误块自带的 font 标签保留。"""
 
     body = render_body(
         business_date=DAY,
@@ -1023,10 +1026,47 @@ def test_body_strips_control_characters_and_angle_brackets() -> None:
         record_no="81",
     )
     assert "\t" not in body
-    assert "<" not in body
-    assert ">" not in body
     assert "a<b>c" in body
     assert "报<表>" in body
+    assert "<font color='red'>" in body
+    assert "</font>" in body
+    # 业务原文里的尖括号不得再以半角出现在公式/实据里。
+    assert "a<b>" not in body
+    assert "报<表>" not in body
+
+
+def test_emphasize_error_block_is_red_bold() -> None:
+    """错误整段套红并逐行加粗。"""
+
+    out = emphasize_verdict_block("报表:TRS005\n结论:错误", VERDICT_ERROR)
+    assert out == (
+        "<font color='red'>"
+        "**报表:TRS005**\n"
+        "**结论:错误**"
+        "</font>"
+    )
+
+
+def test_emphasize_suspect_uses_yellow_tag_on_conclusion() -> None:
+    """可疑与无法展开:正文加粗,结论行用黄标签。"""
+
+    out = emphasize_verdict_block(
+        "节点:订单卖出股数\n结论:可疑。无法判定。",
+        VERDICT_SUSPECT,
+    )
+    assert out == (
+        "**节点:订单卖出股数**\n"
+        "<text_tag color='yellow'>结论:可疑。无法判定。</text_tag>"
+    )
+    assert emphasize_verdict_block("结论:正确", VERDICT_CORRECT) == "结论:正确"
+
+
+def test_emphasize_escapes_stars_inside_bold() -> None:
+    """业务正文里的星号改成实体,避免撕开加粗;下划线保留原文。"""
+
+    out = emphasize_verdict_block("节点:a*b after_val\n结论:错误", VERDICT_ERROR)
+    assert "**节点:a&#42;b after_val**" in out
+    assert "after&#95;val" not in out
 
 
 def test_failure_log_includes_feishu_msg_without_webhook() -> None:

+ 3 - 1
doc/业务链路核对设计.md

@@ -508,6 +508,8 @@ TRS 的字段公式不在 Excel 里,在产品原型 `TRS/4 业务引起数据
 
 章节之间空一行。一张报表或一个字段内部用单个换行,五行连在一起。同一格里有多条期望时,用逗号连成一句,句号收尾。同一份原型文件只写一次,后面接它引用的字段;不同文件仍分行。空的错误、可疑、无法展开各写一行「:无」。
 
+错误结论的分报表块、步骤块、清单逐条块整段 `<font color='red'>` 且逐行 `**加粗**`。可疑与无法展开同款:逐行加粗,结论行用 `<text_tag color='yellow'>`(`lark_md` 正文色只有红/绿/灰,没有黄字)。正确块、空清单「:无」、摘要与章名不加版式标签。业务字段仍先把尖括号改成全角,再套上述标签。
+
 ### 7.3 三张报表
 
 发行、销售、TRS 用同一版式:批次门禁、摘要、分报表、步骤核对、错误清单、可疑清单、无法展开。某一张报表没有结论行时,不编一块「正确」,摘要写明目录里缺了哪个编号。
@@ -516,7 +518,7 @@ TRS 的字段公式不在 Excel 里,在产品原型 `TRS/4 业务引起数据
 
 分报表只收带这些编号的结论行。交收推演、T 日交收、增额主键、增额累计、期初钱包、T 日钱包、候选范围、原始流水、票据与子票据比对是步骤节点,写在「步骤核对」。标题用公式等号左边的步骤名。不拿第一条的票据号、子订单号或币种当报表名。
 
-同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。没有报表编号的,清单里写步骤和对象。主题是错误时,正确列仍留在分报表,不写「正确与可疑留在核对明细」。
+同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论。没有报表编号的,清单里写步骤和对象。主题是错误时,正确列仍留在分报表,不写「正确与可疑留在核对明细」。分报表/步骤总结论为错误或可疑时,整段同样套第 7.2 节的红加粗或黄标签,不只清单逐条。
 
 ```text
 销售报表 错误

+ 1 - 1
doc/使用说明.md

@@ -180,7 +180,7 @@ OTC009、OTC012 是实时导出,没有落库结果,`report_topup_payment`、
 
 三张报表正文是批次门禁、摘要、分报表、步骤核对、错误清单、可疑清单、无法展开。没有门禁行时不写批次门禁。摘要只按 OTC、TRS 编号计张数,并写目录是否到齐。票据号、子订单交收推演、增额、钱包、原始流水是步骤,写在「步骤核对」,标题用步骤名,不叫报表。同一报表、同一依据的正确列收成一块。错误和可疑逐条写节点、期望、实据、依据、结论;没有报表编号的写步骤和对象。方向卡是错误主题时,正确列仍留在分报表。OTC009、OTC012 不进目录。TRS011、TRS015 不进目录。
 
-章节之间空一行。一张报表或一个字段内部用单个换行。期望的多个字段用逗号连成一句。同一份原型只写一次,字段接在文件名后面。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,`<` `>` 会改成全角。发送失败时日志带上飞书返回的 `msg`。
+章节之间空一行。一张报表或一个字段内部用单个换行。期望的多个字段用逗号连成一句。同一份原型只写一次,字段接在文件名后面。空清单写成「错误清单:无」。方向卡不按 3500 字删报表或公式段。请求体超过约 28KB 时按结论行拆成多张,每张带同一个记录编号和「第几张,共几张」,整卡仍是一个 `notify_status`。公式里的制表符会去掉,业务正文里的 `<` `>` 会改成全角。错误块整段用 `lark_md` 红加粗;可疑块与无法展开同款——正文加粗,结论行用黄色 `text_tag`(飞书正文没有黄色字体,只用标签醒目)。正确块不加色。发送失败时日志带上飞书返回的 `msg`。
 
 已有库要先放宽两道 CHECK,再启动。不要重跑 `schema.sql`,那会清空已有轮次。没执行时新的方向卡插不进去,这一轮会整段回滚。