# -*- coding: utf-8 -*- """S0–S8 交付 Word:按 A/B 统一建议另存新版本(旧文件保留)。""" from __future__ import annotations import re import shutil import sys import tempfile from pathlib import Path from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Pt, RGBColor from docx.table import Table from docx.text.paragraph import Paragraph HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) from _add_req_func_codes import ( # noqa: E402 S0_ALIASES, append_code, extract_heading_title, set_paragraph_text, ) from _adjust_front_matter import ( # noqa: E402 clear_page_breaks, ensure_page_break_before, remove_paragraph, ) BASE = Path(r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目管理\产互联项目管理\交付文档") TMP = Path(tempfile.gettempdir()) / "aidop_unify_ab" REPORT = HERE / "_tmp_unify_ab_report.txt" TODAY = "2026-09-04" SYS = "Ai-DOP智慧运营管理系统" AUTHOR = "智造易项目组" NOTE = ( "统一系统名与版本记录;需求标题补 REQ;B 类需求补范围;" "S1 详设补部署/非功能与需求明细核验;数据中台改为数据对接服务;手册指引总纲" ) SYS_REPL = [ ("AI-DOP 智慧运营决策平台", SYS), ("Ai-DOP 智慧运营决策平台", SYS), ("Ai-DOP智慧运营决策平台", SYS), ("Ai-DOP 企业智慧运营管理系统", SYS), ("Ai-DOP企业智慧运营管理系统", SYS), ("Ai-DOP 智慧运营管理系统", SYS), ] DATA_REPL = [ ("数据中台平台(Middle Data Platform)", "数据对接服务(Middle Data Platform)"), ("数据中台平台", "数据对接服务"), ("DOP 数据中台", "DOP 数据对接服务"), ("底层数据中台链路", "底层数据对接服务链路"), ("数据中台三层", "数据对接服务三层"), ("数据中台链路", "数据对接服务链路"), ("数据中台分层", "数据对接服务分层"), ("经数据中台", "经数据对接服务"), ("数据中台 T8", "数据对接服务 T8"), ("数据中台", "数据对接服务"), ] S0_CANON_REQ = { "标准 BOM 管理": "REQ-S0-001", "标准工艺路线管理": "REQ-S0-002", "标准工序管理": "REQ-S0-003", "工作中心管理": "REQ-S0-004", "产线管理": "REQ-S0-005", "产线物料管理": "REQ-S0-006", "产线工位管理": "REQ-S0-007", "人员技能管理": "REQ-S0-008", "人员技能分配": "REQ-S0-009", "前置工序要素管理": "REQ-S0-010", "前置工序要素参数管理": "REQ-S0-011", "生产要素参数管理": "REQ-S0-012", "工艺流程卡管理": "REQ-S0-013", "物料代替方案管理": "REQ-S0-014", "物料替代明细管理(制造域)": "REQ-S0-015", "物料替代明细管理": "REQ-S0-015", "SOP 文件类型管理": "REQ-S0-016", "SOP 维护管理": "REQ-S0-017", "工单控制参数管理": "REQ-S0-018", "订单排程周期管理": "REQ-S0-019", "物料管理": "REQ-S0-020", "客户管理": "REQ-S0-021", "订单评审周期规则管理": "REQ-S0-023", "产品设计周期规则管理": "REQ-S0-024", "合同评审周期规则管理": "REQ-S0-025", "订单优先级规则管理": "REQ-S0-026", "供应商管理": "REQ-S0-027", "采购分类与货源管理": "REQ-S0-028", "品类交货周期管理": "REQ-S0-029", "物料计划周期管理": "REQ-S0-030", "部门管理": "REQ-S0-031", "员工管理": "REQ-S0-032", "员工工作职责管理": "REQ-S0-033", "库位管理": "REQ-S0-034", "库位货架管理": "REQ-S0-035", "物料包装规格管理": "REQ-S0-036", "批次号控制管理": "REQ-S0-037", "批次类型管理": "REQ-S0-038", "标签类型管理": "REQ-S0-039", "成本中心管理": "REQ-S0-040", "任务分配管理": "REQ-S0-041", "检验基准管理": "REQ-S0-042", "检验频次管理": "REQ-S0-043", "检验仪器管理": "REQ-S0-044", "检验项目管理": "REQ-S0-045", "检验方法管理": "REQ-S0-046", "检验标准管理": "REQ-S0-047", "抽样方案管理": "REQ-S0-048", "检验计划管理": "REQ-S0-049", "来料检验规范(IQC)": "REQ-S0-050", "过程检验规范(IPQC)": "REQ-S0-051", "成品检验规范(OQC/FQC)": "REQ-S0-052", "来料白名单管理": "REQ-S0-053", "质量字典管理": "REQ-S0-054", "指标目录管理(KpiMaster)": "REQ-S0-058", "条码规则管理": "REQ-S0-060", "计量器具管理": "REQ-S0-061", "业务事实字典": "REQ-S0-062", "物料工艺要素管理": "REQ-S0-063", "详情看板配置": "REQ-S0-064", } S0_EXTRA_ALIAS = { "运营指标主数据": "指标目录管理(KpiMaster)", "指标关系图谱": "详情看板配置", "检验方案": "检验计划管理", "条码规则": "条码规则管理", "物料工艺要素": "物料工艺要素管理", } S5_REQ = { "来料检验申请查询": "REQ-S5-019", "采购收货单查询": "REQ-S5-004", "生产退料单查询": "REQ-S5-006", "库存查询": "REQ-S5-010", "进出存查询": "REQ-S5-011", "标签查询": "REQ-S5-008", "盘点结果查询与盘点标签确认": "REQ-S5-012", } S6_REQ = { "过程检验单查询与详情": "REQ-S6-002", "IPQC 检验流转(检验员提交 / 主管审核)": "REQ-S6-002", "生产设备台账查询": "REQ-S6-003", "模工具台账查询": "REQ-S6-004", } S7_REQ = { "生产入库单查询": "REQ-S7-003", "销售发货通知查询": "REQ-S7-004", "FQC 检验任务查询": "REQ-S7-001", "FQC 检验结果查询与详情": "REQ-S7-002", } S8_REQ = { "异常监控看板": "REQ-S8-001", "监控大屏(交付 / 生产 / 供应 / 订单执行)": "REQ-S8-002", "异常列表与详情": "REQ-S8-008", "异常处理流转": "REQ-S8-019", "主动提报": "REQ-S8-009", "监视规则配置与运行": "REQ-S8-016", "数据源配置": "REQ-S8-015", "订单执行链路下钻": "REQ-S8-007", "配置中心": "REQ-S8-010", } SCOPE = { "S1": ( "本模块覆盖销售订单评审与交期确认、交付跟踪、发货、工单下达、计划联动、需求明细核验及产销协同看板。", "不维护客户/物料等主数据(见 S0);不编制车间作业计划与报工(见 S2/S6);" "不处理采购到货与来料检验(见 S4/S5);不处理成品出库实物确认(见 S7)。", ), "S2": ( "本模块覆盖生产排程、作业计划及制造协同看板,承接 S1 已下达工单并形成可执行计划。", "不维护工艺/产线主数据(见 S0);不评审销售订单(见 S1);" "不负责车间报工与过程检验判定(见 S6);不处理采购与仓储事务(见 S3–S5)。", ), "S3": ( "本模块覆盖物料需求与交货计划、采购申请、要货令、采购/委外/外协订单及供应协同、齐套看板。", "不维护供应商主数据(见 S0);不处理供应商交货回执与到货入库(见 S4/S5);不排产(见 S2)。", ), "S4": ( "本模块覆盖供应商交货、发货单、采购退货、IQC 退货查询及采购执行看板。", "不编制采购订单与要货令(见 S3);不做到货入库记账与 IQC 判定(见 S5);不维护供应商主数据(见 S0)。", ), } # 现行正式稿已手改:S3 手册截图以 V1.2 为准,勿再从无版本号源重出 V1.1。 SKIP_JOBS = { ("S3", "um", "S3供应协同模块用户操作手册.docx"), } JOBS = [ ("S0", "brd", "S0-运营建模-业务需求描述-V0.6.docx", "V0.6", "V0.7"), ("S0", "bbp", "S0-运营建模-业务蓝图设计方案-V0.6.docx", "V0.6", "V0.7"), ("S0", "dds", "S0运营建模模块详细设计说明书.docx", "V1.0", "V1.1"), ("S0", "um", "S0-运营建模-用户操作手册.docx", "V1.0", "V1.1"), ("S1", "brd", "S1产销协同业务需求描述.docx", "V0.2", "V0.3"), ("S1", "bbp", "S1产销协同模块蓝图设计方案.docx", "V2.0", "V2.1"), ("S1", "dds", "S1产销协同模块详细设计说明书.docx", "V1.0", "V1.1"), ("S1", "um", "S1产销协同模块用户操作手册.docx", "V1.0", "V1.1"), ("S2", "brd", "S2制造协同业务需求描述.docx", "V0.2", "V0.3"), ("S2", "bbp", "S2制造协同模块蓝图设计方案.docx", "V0.2", "V0.3"), ("S2", "dds", "S2制造协同模块详细设计说明书.docx", "V1.0", "V1.1"), ("S2", "um", "S2制造协同模块用户操作手册.docx", "V1.0", "V1.1"), ("S3", "brd", "S3供应协同业务需求描述.docx", "V0.2", "V0.3"), ("S3", "bbp", "S3供应协同模块蓝图设计方案.docx", "V0.2", "V0.3"), ("S3", "dds", "S3供应协同模块详细设计说明书.docx", "V1.0", "V1.1"), ("S3", "um", "S3供应协同模块用户操作手册.docx", "V1.0", "V1.1"), ("S4", "brd", "S4采购执行业务需求描述.docx", "V0.2", "V0.3"), ("S4", "bbp", "S4采购执行模块蓝图设计方案.docx", "V0.2", "V0.3"), ("S4", "dds", "S4采购执行模块详细设计说明书.docx", "V1.0", "V1.1"), ("S4", "um", "S4采购执行模块用户操作手册.docx", "V1.0", "V1.1"), ("S5", "brd", "S5-物料仓储-业务需求描述.docx", "V1.0", "V1.1"), ("S5", "bbp", "S5-物料仓储-业务蓝图设计方案.docx", "V0.1", "V0.2"), ("S5", "dds", "S5物料仓储模块详细设计说明书.docx", "V1.0", "V1.1"), ("S5", "um", "S5-物料仓储-用户操作手册.docx", "V1.0", "V1.1"), ("S6", "brd", "S6-生产执行-业务需求描述.docx", "V1.0", "V1.1"), ("S6", "bbp", "S6-生产执行-业务蓝图设计方案.docx", "V0.1", "V0.2"), ("S6", "dds", "S6生产执行模块详细设计说明书.docx", "V1.0", "V1.1"), ("S6", "um", "S6-生产执行-用户操作手册.docx", "V1.0", "V1.1"), ("S7", "brd", "S7-成品仓储-业务需求描述.docx", "V1.0", "V1.1"), ("S7", "bbp", "S7-成品仓储-业务蓝图设计方案.docx", "V0.1", "V0.2"), ("S7", "dds", "S7成品仓储模块详细设计说明书.docx", "V1.0", "V1.1"), ("S7", "um", "S7-成品仓储-用户操作手册.docx", "V1.0", "V1.1"), ("S8", "brd", "S8-异常监控-业务需求描述.docx", "V1.0", "V1.1"), ("S8", "bbp", "S8-异常监控-业务蓝图设计方案.docx", "V0.1", "V0.2"), ("S8", "dds", "S8异常监控模块详细设计说明书.docx", "V1.0", "V1.1"), ("S8", "um", "S8-异常监控-用户操作手册.docx", "V1.0", "V1.1"), ] def dest_name(src_name: str, new_ver: str) -> str: stem = Path(src_name).stem stem = re.sub(r"-V\d+(?:\.\d+)+$", "", stem) return f"{stem}-{new_ver}.docx" def apply_repl(text: str, pairs) -> str: for a, b in pairs: if a in text: text = text.replace(a, b) return text def iter_paras(doc: Document): for p in doc.paragraphs: yield p for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: yield p def replace_everywhere(doc: Document) -> int: n = 0 for p in iter_paras(doc): old = p.text new = apply_repl(apply_repl(old, SYS_REPL), DATA_REPL) if new != old: set_paragraph_text(p, new) n += 1 return n def body_blocks(doc: Document): for child in doc.element.body: tag = child.tag.split("}")[-1] if tag == "p": yield "p", Paragraph(child, doc) elif tag == "tbl": yield "t", Table(child, doc) def find_p(doc: Document, *names: str) -> Paragraph | None: want = {n.strip() for n in names} for p in doc.paragraphs: if p.text.strip() in want: return p return None def set_cell(cell, text: str, *, bold=False, size=10.5, color=None, fill=None, center=False): cell.text = "" p = cell.paragraphs[0] if center: p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(text) run.font.size = Pt(size) run.bold = bold run.font.name = "微软雅黑" r_pr = run._element.get_or_add_rPr() r_fonts = r_pr.rFonts if r_fonts is None: r_fonts = OxmlElement("w:rFonts") r_pr.insert(0, r_fonts) r_fonts.set(qn("w:ascii"), "微软雅黑") r_fonts.set(qn("w:hAnsi"), "微软雅黑") r_fonts.set(qn("w:eastAsia"), "微软雅黑") if color is not None: run.font.color.rgb = color if fill: tc_pr = cell._element.get_or_add_tcPr() old = tc_pr.find(qn("w:shd")) if old is not None: tc_pr.remove(old) shd = OxmlElement("w:shd") shd.set(qn("w:val"), "clear") shd.set(qn("w:color"), "auto") shd.set(qn("w:fill"), fill) tc_pr.append(shd) def strip_document_control(doc: Document) -> int: removed = 0 drop_next_table = False for kind, obj in list(body_blocks(doc)): if kind == "p": t = obj.text.strip() if t == "文档控制": remove_paragraph(obj) removed += 1 continue if t in {"审核", "发布"}: remove_paragraph(obj) drop_next_table = True removed += 1 continue if t == "更改记录": set_paragraph_text(obj, "版本记录") removed += 1 elif kind == "t" and drop_next_table: obj._tbl.getparent().remove(obj._tbl) drop_next_table = False removed += 1 return removed def update_cover_version(doc: Document, old_ver: str, new_ver: str) -> int: n = 0 for table in doc.tables[:4]: for row in table.rows: cells = [c.text.strip().replace("\n", "") for c in row.cells] if not cells: continue key = cells[0] if key in {"版本", "文档版本", "当前版本"} and len(row.cells) >= 2: set_cell(row.cells[1], new_ver, center=True) n += 1 for i, c in enumerate(row.cells): raw = c.text.strip() if raw == old_ver or raw == f"{old_ver}(草稿)": set_cell(c, new_ver, center=True) n += 1 elif raw.startswith(old_ver) and "日期" not in raw and len(raw) < 20: set_cell(c, raw.replace(old_ver, new_ver, 1), center=True) n += 1 for p in doc.paragraphs[:20]: t = p.text if "当前版本" in t and old_ver in t: set_paragraph_text(p, t.replace(old_ver, new_ver)) n += 1 elif t.startswith("版本:") and old_ver in t: set_paragraph_text(p, t.replace(old_ver, new_ver)) n += 1 elif "编制日期" in t or t.startswith("更新日期"): set_paragraph_text(p, re.sub(r"\d{4}-\d{2}-\d{2}", TODAY, t, count=1)) n += 1 return n def version_table_after(heading: Paragraph) -> Table | None: el = heading._element.getnext() while el is not None: tag = el.tag.split("}")[-1] if tag == "tbl": return Table(el, heading._parent) if tag == "p": txt = "".join(el.itertext()).strip() if txt: return None el = el.getnext() return None def append_version_row(table: Table, new_ver: str) -> None: headers = [c.text.strip().replace("\n", "") for c in table.rows[0].cells] values = { "日期": TODAY, "版本": new_ver, "修订人": AUTHOR, "姓名": AUTHOR, "变更说明": NOTE, } # reuse last empty data row if present target = None if len(table.rows) > 1: last = table.rows[-1] if not any(c.text.strip() for c in last.cells): target = last if target is None: target = table.add_row() for i, h in enumerate(headers): if i < len(target.cells) and h in values: set_cell(target.cells[i], values[h], size=10.5) def _ensure_tbl_borders(table: Table) -> None: tbl = table._tbl tbl_pr = tbl.tblPr if tbl_pr is None: tbl_pr = OxmlElement("w:tblPr") tbl.insert(0, tbl_pr) borders = tbl_pr.find(qn("w:tblBorders")) if borders is None: borders = OxmlElement("w:tblBorders") tbl_pr.append(borders) for edge in ("top", "left", "bottom", "right", "insideH", "insideV"): el = borders.find(qn(f"w:{edge}")) if el is None: el = OxmlElement(f"w:{edge}") borders.append(el) el.set(qn("w:val"), "single") el.set(qn("w:sz"), "4") el.set(qn("w:space"), "0") el.set(qn("w:color"), "000000") def make_version_table(doc: Document, heading: Paragraph, old_ver: str, new_ver: str) -> None: table = doc.add_table(rows=3, cols=4) try: table.style = "Table Grid" except (KeyError, ValueError): _ensure_tbl_borders(table) headers = ["日期", "版本", "修订人", "变更说明"] for i, h in enumerate(headers): set_cell(table.rows[0].cells[i], h, bold=True, color=RGBColor(255, 255, 255), fill="1F3864", center=True) set_cell(table.rows[1].cells[0], "2026-06-10") set_cell(table.rows[1].cells[1], old_ver) set_cell(table.rows[1].cells[2], AUTHOR) set_cell(table.rows[1].cells[3], "既有版本") set_cell(table.rows[2].cells[0], TODAY) set_cell(table.rows[2].cells[1], new_ver) set_cell(table.rows[2].cells[2], AUTHOR) set_cell(table.rows[2].cells[3], NOTE) heading._element.addnext(table._tbl) def ensure_version_page(doc: Document, old_ver: str, new_ver: str) -> str: heading = find_p(doc, "版本记录", "更改记录") if heading is not None: if heading.text.strip() == "更改记录": set_paragraph_text(heading, "版本记录") tbl = version_table_after(heading) if tbl is not None: append_version_row(tbl, new_ver) ensure_page_break_before(heading) toc = find_p(doc, "目录") if toc is not None: ensure_page_break_before(toc) return "append" toc = find_p(doc, "目录") if toc is None: return "skip" heading = toc.insert_paragraph_before("版本记录") heading.style = toc.style if toc.style and toc.style.name.startswith("Heading") else "Heading 1" make_version_table(doc, heading, old_ver, new_ver) ensure_page_break_before(heading) ensure_page_break_before(toc) return "insert" def s0_req_code(title: str) -> str | None: if title in S0_CANON_REQ: return S0_CANON_REQ[title] if title in S0_EXTRA_ALIAS: return S0_CANON_REQ.get(S0_EXTRA_ALIAS[title]) if title in S0_ALIASES: return S0_CANON_REQ.get(S0_ALIASES[title]) return None def add_req_codes(doc: Document, module: str) -> int: mapping = { "S0": None, "S5": S5_REQ, "S6": S6_REQ, "S7": S7_REQ, "S8": S8_REQ, }.get(module) n = 0 for p in doc.paragraphs: raw = p.text.strip() if not raw or "REQ-S" in raw: continue title = extract_heading_title(raw) if module == "S8" and re.match(r"^[一二三四五六七八九十]+、", raw): title = re.sub(r"^[一二三四五六七八九十]+、", "", raw).strip() if not title: continue code = s0_req_code(title) if module == "S0" else (mapping or {}).get(title) if code and append_code(p, code): n += 1 return n def insert_scope(doc: Document, module: str) -> int: if module not in SCOPE: return 0 if any("本模块覆盖" in p.text for p in doc.paragraphs[:40]): return 0 target = None for p in doc.paragraphs: if re.match(r"^(一)", p.text.strip()): target = p break if target is None: return 0 do, dont = SCOPE[module] p3 = target.insert_paragraph_before(f"本模块不做什么:{dont}") p2 = target.insert_paragraph_before(f"本模块做什么:{do}") p1 = target.insert_paragraph_before("范围说明") try: p1.style = "Heading 2" except Exception: pass return 3 def insert_login_pointer(doc: Document) -> int: marker = None for p in doc.paragraphs: t = p.text.strip() if t in {"1 登录与首页", "1. 系统登录与首页"}: marker = p break if marker is None: return 0 el = marker._element.getnext() while el is not None and el.tag.split("}")[-1] != "p": el = el.getnext() if el is not None and "SYS-用户操作手册-总纲" in "".join(el.itertext()): return 0 text = ( "登录、修改密码、退出等通用操作见《SYS-用户操作手册-总纲》。" "本章仅说明进入本模块后的页面位置;详细登录步骤以总纲为准。" ) if el is not None: Paragraph(el, marker._parent).insert_paragraph_before(text) else: marker.insert_paragraph_before(text) return 1 def insert_um_common(doc: Document) -> int: if any("通用操作说明" in p.text for p in doc.paragraphs[:80]): return 0 target = None for p in doc.paragraphs: t = p.text.strip() if t in {"2. 模块操作说明", "2.模块操作说明"} or re.match(r"^2\.1\s", t): target = p break if target is None: return 0 lines = [ "在查询区填写条件后点查询筛选数据,点重置清空条件并回到默认结果。", "列表底部显示总条数,可翻页或切换每页条数;操作列按行提供查看或编辑入口。", "录入窗口中红色星号为必填项,未填时保存会提示,不会生成不完整单据。", ] for line in reversed(lines): target.insert_paragraph_before(line) h = target.insert_paragraph_before("2.0 通用操作说明") try: h.style = "Heading 2" except Exception: pass return 4 def patch_s1_dds(doc: Document) -> int: n = 0 # rename appendix 7 -> 9 first for p in doc.paragraphs: t = p.text nt = t nt = re.sub(r"^7\.3\s", "9.3 ", nt) nt = re.sub(r"^7\.2\s", "9.2 ", nt) nt = re.sub(r"^7\.1\s", "9.1 ", nt) nt = re.sub(r"^7\.\s*附录", "9. 附录", nt) if nt != t: set_paragraph_text(p, nt) n += 1 # renumber 3.2.x via placeholders mapping = [ ("3.2.8", "@@A@@"), ("3.2.7", "@@B@@"), ("3.2.5", "@@C@@"), ("3.2.4", "@@D@@"), ("3.2.3", "@@E@@"), ("3.2.2", "@@F@@"), ] final = { "@@A@@": "3.2.7", "@@B@@": "3.2.5", "@@C@@": "3.2.4", "@@D@@": "3.2.3", "@@E@@": "3.2.2", "@@F@@": "3.2.1", } for p in iter_paras(doc): t = p.text nt = t for a, b in mapping: nt = nt.replace(a, b) for a, b in final.items(): nt = nt.replace(a, b) if nt != t: set_paragraph_text(p, nt) n += 1 # insert 3.2.6 before 3.2.7 产销协同看板 target = None for p in doc.paragraphs: if p.text.strip().startswith("3.2.7") and "产销协同看板" in p.text: target = p break if target is not None and not any("3.2.6" in x.text and "需求明细核验" in x.text for x in doc.paragraphs): blocks = [ ("Heading 3", "3.2.6 需求明细核验(S1-M08)"), ("Heading 3", "3.2.6.1 业务流程"), ("Normal", "计划员在交期确认前核验订单需求明细与成品库存、在制及物料齐套是否一致;有差异时先解释差异再确认交期。"), ("Heading 3", "3.2.6.2 功能处理"), ("Normal", "核验须绑定订单号与需求行;数量、交期、物料编码为空不得提交。核验结论与系统计算不一致时必须填写原因。无授权不得改写需求数量。"), ("Heading 3", "3.2.6.3 页面设计"), ("Normal", "列表按订单号、客户、物料筛选;详情展示需求行、齐套结论与差异说明。"), ("Heading 3", "3.2.6.4 设计验证要点"), ("Normal", "缺必填不可保存;差异无原因不可确认;核验结果可在订单评审与计划联动中追溯。"), ] for style, text in blocks: np = target.insert_paragraph_before(text) try: np.style = style except Exception: pass n += 1 app = None for p in doc.paragraphs: if p.text.strip().startswith("9.") and "附录" in p.text: app = p break if app is not None and not any("SaaS" in x.text for x in doc.paragraphs): blocks = [ ("Heading 1", "7. SaaS部署与运维设计"), ("Heading 2", "7.1 部署模式和环境"), ("Normal", "S1 随 Ai-DOP智慧运营管理系统统一 SaaS 多租户部署,不单独建库;开发、预发、生产环境配置隔离。"), ("Heading 2", "7.2 部署架构"), ("Normal", "采用容器化部署:反向代理 + 前端静态资源 + .NET API + MySQL + Redis。S1 业务服务随平台一同发布。"), ("Heading 2", "7.3 租户开通和初始化"), ("Normal", "新租户开通后初始化组织、角色与 S1 菜单权限;订单评审等业务数据按租户隔离,禁止跨租户可见。"), ("Heading 2", "7.4 发布、升级和回滚"), ("Normal", "发版随平台版本号发布;升级失败按平台回滚策略回退至上一可用版本,保留订单与评审记录。"), ("Heading 2", "7.5 监控、备份和恢复"), ("Normal", "监控接口时延、排程/评审任务失败率与同步延迟;数据库按平台策略备份,恢复后核对订单与交期结论。"), ("Heading 1", "8. 非功能设计"), ("Normal", "性能:列表默认分页,常用查询应在可接受时间内返回。安全:接口鉴权、操作审计、租户隔离。可用性:关键操作失败须给出业务原因,不得静默丢单。可维护性:需求、蓝图、详设编号与 FUNC/REQ 对齐。"), ] for style, text in blocks: np = app.insert_paragraph_before(text) try: np.style = style except Exception: pass n += 1 return n def ensure_sys_cover(doc: Document) -> int: early = [p.text for p in doc.paragraphs[:15]] if any(SYS in t for t in early): return 0 first = None for p in doc.paragraphs[:8]: if p.text.strip(): first = p break if first is None: return 0 el = first._element.getnext() while el is not None and el.tag.split("}")[-1] != "p": el = el.getnext() if el is None: return 0 np = Paragraph(el, first._parent).insert_paragraph_before(SYS) np.alignment = WD_ALIGN_PARAGRAPH.CENTER return 1 def open_via_temp(src: Path) -> tuple[Document, Path]: TMP.mkdir(parents=True, exist_ok=True) work = TMP / f"in_{src.stem[:40]}.docx" shutil.copy2(src, work) return Document(str(work)), work def process_job(module: str, kind: str, src_name: str, old_ver: str, new_ver: str) -> str: src = BASE / module / src_name if not src.exists(): return f"MISSING {src}" dest = BASE / module / dest_name(src_name, new_ver) doc, work = open_via_temp(src) notes = [] notes.append(f"repl={replace_everywhere(doc)}") notes.append(f"syscover={ensure_sys_cover(doc)}") notes.append(f"ctrl={strip_document_control(doc)}") notes.append(f"cover={update_cover_version(doc, old_ver, new_ver)}") notes.append(f"verpage={ensure_version_page(doc, old_ver, new_ver)}") if kind == "brd" and module in {"S0", "S5", "S6", "S7", "S8"}: notes.append(f"req={add_req_codes(doc, module)}") if kind == "brd" and module in SCOPE: notes.append(f"scope={insert_scope(doc, module)}") if kind == "dds" and module == "S1": notes.append(f"s1dds={patch_s1_dds(doc)}") if kind == "um": notes.append(f"login={insert_login_pointer(doc)}") if module in {"S1", "S2", "S3", "S4"}: notes.append(f"common={insert_um_common(doc)}") out = TMP / f"out_{dest.stem[:40]}.docx" doc.save(str(out)) shutil.copy2(out, dest) return f"OK {dest.name} ({'; '.join(notes)}) {dest.stat().st_size // 1024}KB" def main() -> None: TMP.mkdir(parents=True, exist_ok=True) lines = [f"TODAY={TODAY}", ""] only = None for arg in sys.argv[1:]: if arg.startswith("--only="): only = {x.strip().upper() for x in arg.split("=", 1)[1].split(",") if x.strip()} for module, kind, src_name, old_ver, new_ver in JOBS: if only and module not in only: continue if (module, kind, src_name) in SKIP_JOBS: msg = f"SKIP {module} {kind} {src_name} (hand-maintained)" print(msg, flush=True) lines.append(msg) continue print(f"processing {module} {kind} {src_name} -> {new_ver}", flush=True) try: msg = process_job(module, kind, src_name, old_ver, new_ver) except Exception as exc: msg = f"FAIL {module} {src_name}: {exc}" print(msg, flush=True) lines.append(msg) REPORT.write_text("\n".join(lines), encoding="utf-8") print("report", REPORT) if __name__ == "__main__": main()