| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- #!/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()
|