Files
zkxw/backend/scripts/apply_sop_row_hidden.py
T
2026-08-28 17:38:37 +08:00

236 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""为 SOP 模板正文表非主操作区行添加 sop-row-hidden,并重新生成 seed SQL。"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
TEMPLATES_DIR = ROOT / "docs" / "sop-templates"
SQL_DIR = ROOT / "docs" / "sql"
SOP_CODES = [
"SOP-CC-001-E01-01",
"SOP-CC-002-E01-01",
"SOP-CC-003-E01-01",
"SOP-CC-006-E01-01",
"SOP-SC-003-E01-01",
"SOP-SC-004-E01-01",
"SOP-SC-005-E01-01",
"SOP-SC-006-E01-01",
"SOP-SC-007-E01-01",
"SOP-SC-008-E01-01",
"SOP-SC-009-E01-01",
"SOP-SC-010-E01-01",
"SOP-SC-011-E01-01",
"SOP-SC-012-E01-01",
"SOP-SC-013-E01-01",
"SOP-SC-014-E01-01",
]
SC014_NK_ROW_OLD = (
'<tr>\n'
' <td style="border:1px solid #334155; padding:8px; text-align:center; vertical-align:middle;">'
"NK细胞<br />培养</td>\n"
' <td colspan="11" style="border:1px solid #334155; padding:10px; vertical-align:top; word-wrap:break-word; overflow-wrap:break-word;">'
)
SC014_NK_ROW_NEW = (
'<tr class="sop-fill-row">\n'
' <td class="sop-fill-label" style="border:1px solid #334155; padding:8px; text-align:center; vertical-align:middle; height:100%;">'
"NK细胞<br />培养</td>\n"
' <td class="sop-fill-cell" colspan="11" style="border:1px solid #334155; padding:10px; vertical-align:top; height:100%; word-wrap:break-word; overflow-wrap:break-word;">'
)
def _is_tr_open(html: str, pos: int) -> bool:
if not html.startswith("<tr", pos):
return False
nxt = html[pos + 3 : pos + 4]
return nxt in ("", " ", ">", "/", "\n", "\t", "\r")
def _split_top_level_trs(html: str) -> list[str]:
rows: list[str] = []
i = 0
n = len(html)
while i < n:
start = html.find("<tr", i)
if start < 0 or not _is_tr_open(html, start):
break
depth = 0
pos = start
while pos < n:
if _is_tr_open(html, pos):
depth += 1
pos += 3
continue
if html.startswith("</tr>", pos):
depth -= 1
pos += 5
if depth == 0:
rows.append(html[start:pos])
i = pos
break
continue
pos += 1
else:
raise ValueError("unclosed <tr> in sop-body-table tbody")
return rows
def _add_hidden_class(tr_html: str) -> str:
open_tag = tr_html.split(">", 1)[0] + ">"
if "sop-fill-row" in open_tag:
return tr_html
def _patch_tr_tag(match: re.Match[str]) -> str:
tag = match.group(0)
if 'class="' in tag:
return re.sub(
r'class="([^"]*)"',
lambda m: f'class="{m.group(1)} sop-row-hidden"',
tag,
count=1,
)
return tag.replace("<tr", '<tr class="sop-row-hidden"', 1)
return re.sub(r"<tr[^>]*>", _patch_tr_tag, tr_html, count=1)
def _find_body_tbody_span(html: str) -> tuple[int, int] | None:
body_start = html.find('<table class="sop-body-table"')
if body_start < 0:
return None
tbody_open = html.find("<tbody>", body_start)
if tbody_open < 0:
return None
inner_start = tbody_open + len("<tbody>")
table_depth = 1
tbody_depth = 1
pos = inner_start
n = len(html)
while pos < n:
if html.startswith("<table", pos):
table_depth += 1
pos += 6
continue
if html.startswith("</table>", pos):
table_depth -= 1
pos += 8
continue
if html.startswith("<tbody>", pos):
tbody_depth += 1
pos += 7
continue
if html.startswith("</tbody>", pos):
tbody_depth -= 1
pos += 8
if tbody_depth == 0:
return inner_start, pos - 8
continue
pos += 1
raise ValueError("无法定位 sop-body-table 的 tbody 结束位置")
def process_body_table(html: str) -> str:
span = _find_body_tbody_span(html)
if span is None:
return html
inner_start, inner_end = span
tbody_inner = html[inner_start:inner_end]
rows = _split_top_level_trs(tbody_inner)
processed = [_add_hidden_class(row) for row in rows]
return html[:inner_start] + "".join(processed) + html[inner_end:]
def process_template(html: str, code: str) -> str:
html = html.replace(
'<table class="sop-header-table"',
'<table class="sop-header-table sop-row-hidden"',
1,
)
if code == "SOP-SC-014-E01-01":
html = html.replace(SC014_NK_ROW_OLD, SC014_NK_ROW_NEW)
html = html.replace("sop-a4-pages-2", "sop-a4-pages-1")
html = re.sub(
r"min-height:\s*594mm;\s*height:\s*auto;",
"height: 297mm; min-height: 297mm;",
html,
)
return process_body_table(html)
def sql_escape(html: str) -> str:
return html.replace("\\", "\\\\").replace("'", "''")
def write_seed_sql(code: str, html: str) -> None:
short = code.replace("SOP-", "").replace("-E01-01", "").lower()
sql_path = SQL_DIR / f"2026-08-25-seed-sop-{short}-content.sql"
comment = html.split("\n", 1)[0].strip().removeprefix("<!-- ").removesuffix(" -->")
escaped = sql_escape(html)
sql_path.write_text(
"\n".join(
[
f"-- 种子数据: {code} {comment.split('(')[0].strip()}",
f"-- 源文件: docs/sop-templates/{code}.html",
"",
"UPDATE `biz_process`",
"SET `sop_content` = '"
f"{escaped}"
"',",
" `update_time` = NOW()",
f"WHERE `process_code` = '{code}'",
" AND `del_flag` = 0;",
"",
]
),
encoding="utf-8",
)
def write_deploy_bundle() -> Path:
"""合并 seed + sync SQL,供测试/生产一次性执行。"""
deploy_path = SQL_DIR / "2026-08-28-sop-visible-section-only-deploy.sql"
parts = [
"-- =============================================================================",
"-- SOP 精简展示:仅保留主操作记录区(页眉与其余正文行均已隐藏)",
"-- 生成时间: 由 backend/scripts/apply_sop_row_hidden.py 自动生成",
"-- 执行顺序: 先 seed 工艺库 → 再 sync 路线/任务步骤",
"--",
"-- 【生产环境】推荐方式(二选一):",
"-- A) mysql -h <host> -u <user> -p <dbname> < docs/sql/2026-08-28-sop-visible-section-only-deploy.sql",
"-- B) ENV_FILE=.env.production python3 backend/scripts/run_sop_visible_deploy.py",
"-- =============================================================================",
"",
]
for code in SOP_CODES:
short = code.replace("SOP-", "").replace("-E01-01", "").lower()
seed = SQL_DIR / f"2026-08-25-seed-sop-{short}-content.sql"
sync = SQL_DIR / f"2026-08-25-sync-sop-{short}-content-to-routes-tasks.sql"
parts.append(f"-- ---------- {code} seed ----------")
parts.append(seed.read_text(encoding="utf-8").strip())
parts.append("")
if sync.exists():
parts.append(f"-- ---------- {code} sync ----------")
parts.append(sync.read_text(encoding="utf-8").strip())
parts.append("")
deploy_path.write_text("\n".join(parts) + "\n", encoding="utf-8")
return deploy_path
def main() -> None:
for code in SOP_CODES:
path = TEMPLATES_DIR / f"{code}.html"
html = path.read_text(encoding="utf-8")
updated = process_template(html, code)
path.write_text(updated, encoding="utf-8")
write_seed_sql(code, updated)
print(f"{code}: done")
deploy = write_deploy_bundle()
print(f"deploy bundle: {deploy}")
if __name__ == "__main__":
main()