# -*- coding: utf-8 -*- from pathlib import Path from datetime import date from PIL import Image from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE from pptx.enum.text import PP_ALIGN from pptx.util import Inches, Pt BASE = Path(__file__).resolve().parent ASSET_DIR = BASE / "ppt_assets" OUT = BASE / "MESCommonBase迁移分析资料包.pptx" COLORS = { "bg": RGBColor(247, 249, 252), "navy": RGBColor(17, 24, 39), "text": RGBColor(52, 64, 84), "muted": RGBColor(102, 112, 133), "line": RGBColor(199, 210, 229), "blue": RGBColor(40, 100, 180), "green": RGBColor(34, 122, 69), "purple": RGBColor(126, 34, 206), "amber": RGBColor(178, 119, 0), "red": RGBColor(192, 16, 72), "blue_light": RGBColor(232, 241, 255), "green_light": RGBColor(234, 247, 239), "purple_light": RGBColor(243, 232, 255), "amber_light": RGBColor(255, 247, 230), "red_light": RGBColor(255, 241, 243), "white": RGBColor(255, 255, 255), } FONT = "Microsoft YaHei" FONT_ALT = "SimHei" def crop_images(): ASSET_DIR.mkdir(exist_ok=True) specs = [ ( "MESCommonBase流程图.png", [ ("总流程_完整.png", None), ("总流程_入口解析.png", (1450, 0, 3800, 1450)), ("总流程_Type分支.png", (110, 1450, 5100, 2580)), ("总流程_响应默认.png", (190, 2600, 5000, 3520)), ], ), ( "MESCommonBase响应输出与SqlWebCall主流程图.png", [ ("响应主_完整.png", None), ("响应主_分支出口.png", (120, 760, 5280, 2050)), ("响应主_规则异常.png", (300, 2100, 5150, 2880)), ], ), ( "MESCommonBase响应输出与SqlWebCall次流程图.png", [ ("SqlWebCall_完整.png", None), ("SqlWebCall_初始化.png", (1500, 120, 4750, 820)), ("SqlWebCall_Type二次分发.png", (110, 900, 5700, 2600)), ("SqlWebCall_数据库返回.png", (260, 2680, 5550, 3540)), ("SqlWebCall_风险.png", (520, 3650, 5300, 3950)), ], ), ] for src_name, crops in specs: src = BASE / src_name if not src.exists(): continue with Image.open(src) as im: for out_name, box in crops: out = ASSET_DIR / out_name if box is None: im.copy().save(out) else: im.crop(box).save(out) def set_font(paragraph, size=None, bold=None, color=None): for run in paragraph.runs: run.font.name = FONT try: run._r.rPr.rFonts.set("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia", FONT) except Exception: pass if size is not None: run.font.size = Pt(size) if bold is not None: run.font.bold = bold if color is not None: run.font.color.rgb = color def apply_run_font(run, size, color=COLORS["text"], bold=False): run.font.name = FONT try: run._r.rPr.rFonts.set("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}eastAsia", FONT) except Exception: pass run.font.size = Pt(size) run.font.color.rgb = color run.font.bold = bold class Deck: def __init__(self): self.prs = Presentation() self.prs.slide_width = Inches(13.333) self.prs.slide_height = Inches(7.5) self.slide_no = 0 def save(self): self.prs.save(OUT) def blank(self): slide = self.prs.slides.add_slide(self.prs.slide_layouts[6]) self.slide_no += 1 bg = slide.background.fill bg.solid() bg.fore_color.rgb = COLORS["bg"] return slide def add_footer(self, slide, section="MESCommonBase 迁移分析资料包"): tx = slide.shapes.add_textbox(Inches(0.45), Inches(7.12), Inches(9.5), Inches(0.25)) p = tx.text_frame.paragraphs[0] run = p.add_run() run.text = section apply_run_font(run, 8, COLORS["muted"]) tx2 = slide.shapes.add_textbox(Inches(12.0), Inches(7.12), Inches(0.85), Inches(0.25)) p2 = tx2.text_frame.paragraphs[0] p2.alignment = PP_ALIGN.RIGHT run2 = p2.add_run() run2.text = str(self.slide_no) apply_run_font(run2, 8, COLORS["muted"]) def title(self, slide, title, subtitle=None): tx = slide.shapes.add_textbox(Inches(0.55), Inches(0.32), Inches(12.2), Inches(0.65)) p = tx.text_frame.paragraphs[0] run = p.add_run() run.text = title apply_run_font(run, 26, COLORS["navy"], True) if subtitle: st = slide.shapes.add_textbox(Inches(0.58), Inches(0.92), Inches(12.0), Inches(0.32)) p2 = st.text_frame.paragraphs[0] run2 = p2.add_run() run2.text = subtitle apply_run_font(run2, 10.5, COLORS["muted"]) def section_slide(self, title, subtitle): slide = self.blank() bar = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, 0, 0, Inches(13.333), Inches(7.5)) bar.fill.solid() bar.fill.fore_color.rgb = RGBColor(238, 244, 255) bar.line.color.rgb = RGBColor(238, 244, 255) tx = slide.shapes.add_textbox(Inches(1.1), Inches(2.45), Inches(11.0), Inches(0.75)) p = tx.text_frame.paragraphs[0] run = p.add_run() run.text = title apply_run_font(run, 34, COLORS["navy"], True) st = slide.shapes.add_textbox(Inches(1.12), Inches(3.28), Inches(10.5), Inches(0.45)) p2 = st.text_frame.paragraphs[0] run2 = p2.add_run() run2.text = subtitle apply_run_font(run2, 15, COLORS["text"]) self.add_footer(slide) return slide def card(self, slide, x, y, w, h, title, body=None, fill="white", line="line", title_color="navy"): shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) shape.fill.solid() shape.fill.fore_color.rgb = COLORS[fill] shape.line.color.rgb = COLORS[line] shape.line.width = Pt(1.2) tx = slide.shapes.add_textbox(Inches(x + 0.18), Inches(y + 0.13), Inches(w - 0.36), Inches(h - 0.22)) tf = tx.text_frame tf.word_wrap = True p = tf.paragraphs[0] run = p.add_run() run.text = title apply_run_font(run, 13, COLORS[title_color], True) if body: p2 = tf.add_paragraph() p2.space_before = Pt(4) run2 = p2.add_run() run2.text = body apply_run_font(run2, 9.5, COLORS["text"]) return shape def bullets(self, slide, x, y, w, h, items, font_size=13, color=COLORS["text"], title=None): tx = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) tf = tx.text_frame tf.word_wrap = True tf.clear() if title: p = tf.paragraphs[0] run = p.add_run() run.text = title apply_run_font(run, font_size + 2, COLORS["navy"], True) for idx, item in enumerate(items): p = tf.add_paragraph() if title or idx else tf.paragraphs[0] p.text = "" p.level = 0 p.space_after = Pt(3) run = p.add_run() run.text = f"• {item}" apply_run_font(run, font_size, color) return tx def add_image_fit(self, slide, image_name, x, y, w, h, border=True): path = ASSET_DIR / image_name if not path.exists(): return None with Image.open(path) as im: iw, ih = im.size max_w, max_h = Inches(w), Inches(h) scale = min(max_w / iw, max_h / ih) pic_w, pic_h = int(iw * scale), int(ih * scale) left = Inches(x) + int((max_w - pic_w) / 2) top = Inches(y) + int((max_h - pic_h) / 2) if border: rect = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) rect.fill.solid() rect.fill.fore_color.rgb = COLORS["white"] rect.line.color.rgb = COLORS["line"] rect.line.width = Pt(1.1) return slide.shapes.add_picture(str(path), left, top, width=pic_w, height=pic_h) def table(self, slide, x, y, w, h, data, col_widths=None, font_size=8.5): rows, cols = len(data), len(data[0]) shape = slide.shapes.add_table(rows, cols, Inches(x), Inches(y), Inches(w), Inches(h)) table = shape.table if col_widths: for idx, cw in enumerate(col_widths): table.columns[idx].width = Inches(cw) for r, row in enumerate(data): for c, value in enumerate(row): cell = table.cell(r, c) cell.text = str(value) cell.margin_left = Inches(0.05) cell.margin_right = Inches(0.05) fill = cell.fill fill.solid() fill.fore_color.rgb = RGBColor(238, 244, 255) if r == 0 else COLORS["white"] for p in cell.text_frame.paragraphs: p.alignment = PP_ALIGN.CENTER if r == 0 else PP_ALIGN.LEFT for run in p.runs: apply_run_font(run, font_size, COLORS["navy"] if r == 0 else COLORS["text"], r == 0) return shape def build_deck(): crop_images() deck = Deck() # 1 cover slide = deck.blank() bg = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, 0, 0, Inches(13.333), Inches(7.5)) bg.fill.solid() bg.fill.fore_color.rgb = RGBColor(238, 244, 255) bg.line.color.rgb = RGBColor(238, 244, 255) tx = slide.shapes.add_textbox(Inches(0.9), Inches(1.45), Inches(11.6), Inches(1.0)) p = tx.text_frame.paragraphs[0] run = p.add_run() run.text = "MESCommonBase 迁移分析资料包" apply_run_font(run, 36, COLORS["navy"], True) st = slide.shapes.add_textbox(Inches(0.95), Inches(2.45), Inches(10.8), Inches(0.72)) p2 = st.text_frame.paragraphs[0] run2 = p2.add_run() run2.text = "响应输出、SqlWebCall 默认分支、参数协议、数据库链路与移植检查清单" apply_run_font(run2, 17, COLORS["text"]) deck.card(slide, 0.95, 4.6, 3.2, 1.05, "分析对象", "MES_Manage/submit/MESCommonBase.ashx", "white", "blue", "blue") deck.card(slide, 4.45, 4.6, 3.2, 1.05, "资料来源", "working 目录中的梳理文档、流程图和源码定位", "white", "green", "green") deck.card(slide, 7.95, 4.6, 3.2, 1.05, "用途", "为项目移植、风险评估和接口改造提供资料", "white", "purple", "purple") deck.add_footer(slide) # 2 deck map slide = deck.blank() deck.title(slide, "资料包结构", "从可视化流程、程序机制、风险到移植步骤,按实际迁移顺序组织。") cards = [ ("1. 范围与入口", "程序定位、资料文件、核心依赖", "blue_light", "blue"), ("2. 主流程", "请求解析、Type 首层分发、响应出口", "green_light", "green"), ("3. SqlWebCall", "初始化、二次分发、参数处理、数据库执行", "purple_light", "purple"), ("4. 迁移资料", "配置、依赖、测试矩阵、风险和建议", "amber_light", "amber"), ] for i, (title, body, fill, line) in enumerate(cards): deck.card(slide, 0.8 + i * 3.1, 1.65, 2.75, 1.3, title, body, fill, line, line.replace("_light", "") if "_light" in line else line) deck.bullets( slide, 0.9, 3.45, 11.8, 2.25, [ "PPT 中的流程图采用完整图 + 局部放大方式,避免大图缩放后看不清。", "Markdown 文档保留在 working 中,PPT 只提取迁移分析需要的结论和路径。", "源 Mermaid 文件、PNG、渲染脚本一并保留,后续可继续改图或补充页面。", ], font_size=14, ) deck.add_footer(slide) # 3 file index slide = deck.blank() deck.title(slide, "working 目录资料索引", "PPT 已整合这些文件,原文件仍保留作为可追溯资料。") data = [ ["文件", "用途"], ["MESCommonBase程序梳理.md", "完整梳理入口、Type 分支、文件上传下载和风险点"], ["MESCommonBase流程图.mmd / .png", "主程序总览流程图"], ["MESCommonBase响应输出与SqlWebCall默认分支详细文档.md", "响应输出和 SqlWebCall 默认分支专项说明"], ["响应输出与SqlWebCall主流程图.mmd / .png", "主图:首层分支与 HTTP 响应出口"], ["响应输出与SqlWebCall次流程图.mmd / .png", "次图:SqlWebCall 内部二次分发与数据库链路"], ["render_*.py / build_*.py", "流程图和 PPT 自动生成脚本"], ] deck.table(slide, 0.7, 1.35, 11.95, 4.9, data, col_widths=[4.4, 7.55], font_size=9.2) deck.add_footer(slide) # 4 program positioning slide = deck.blank() deck.title(slide, "程序定位与迁移目标", "MESCommonBase 是通用提交网关,不是单一业务接口。") deck.card(slide, 0.75, 1.35, 3.7, 4.7, "程序定位", "ASP.NET WebHandler\n实现 IHttpHandler\n入口:ProcessRequest\n根据 Type 分流", "blue_light", "blue", "blue") deck.card(slide, 4.75, 1.35, 3.7, 4.7, "关键职责", "文件导出/下载\n文件上传\nIP 查询\n通用数据库调用\n响应统一写出", "green_light", "green", "green") deck.card(slide, 8.75, 1.35, 3.7, 4.7, "移植目标", "保留协议兼容\n迁移依赖 DLL 和配置\n还原数据库执行能力\n补齐风险控制和验证用例", "purple_light", "purple", "purple") deck.add_footer(slide) # 5 source dependencies slide = deck.blank() deck.title(slide, "程序依赖关系", "移植时需要同时关注 Web 入口、业务库、基础库、Excel 生成库和 Web.config。") data = [ ["依赖", "迁移关注点"], ["MES_Manage/submit/MESCommonBase.ashx", "WebHandler 入口,需部署到 submit 路径"], ["DataLinkMesWork.dll / 源码", "SqlWebCall、存储过程、SQL 执行、文件上传下载"], ["BasicData.dll / jsonobj", "旧协议 DTO,Type/Name/Param/token 等字段"], ["WriteExcelNPOI.dll / ExcelWebCall", "2001/2002/2003 Excel/PDF 导出"], ["Newtonsoft.Json、NPOI、Spire、Lazy.Captcha 等", "bin 目录运行依赖,版本需一致或验证兼容"], ["Web.config appSettings ConnectionString", "默认连接串来源,当前使用明文配置"], ] deck.table(slide, 0.65, 1.35, 12.05, 4.95, data, col_widths=[4.0, 8.05], font_size=9.1) deck.add_footer(slide) # 6 section deck.section_slide("第一部分:主流程与响应输出", "说明 MESCommonBase 如何从 type 分支走向二进制下载、JSON 文本、IP 文本和异常兜底。") # 7 full overview slide = deck.blank() deck.title(slide, "MESCommonBase 总体流程图", "完整图用于把入口解析、Type 分支、响应出口和风险位置放在一张图中查看。") deck.add_image_fit(slide, "总流程_完整.png", 0.55, 1.15, 12.25, 5.85) deck.add_footer(slide) # 8 entry crop slide = deck.blank() deck.title(slide, "入口解析与 Type 识别", "迁移时要保持三种入参来源的兼容:JSON body、jsonobj、Request[\"param\"]。") deck.add_image_fit(slide, "总流程_入口解析.png", 0.65, 1.2, 7.0, 4.9) deck.bullets( slide, 8.0, 1.45, 4.55, 4.6, [ "默认 ContentType 设置为 application/json。", "先用 JsonMapper 解析,再尝试反序列化 jsonobj。", "multipart/form-data 场景通常依赖 Request[\"param\"]。", "解析失败被吞掉,type 可能保持默认 0 并进入 default。", ], font_size=13.2, title="入口兼容点", ) deck.add_footer(slide) # 9 type branch crop slide = deck.blank() deck.title(slide, "Type 首层分支总览", "首层分支决定响应出口:文件流、上传 JSON、IP 文本或进入 SqlWebCall。") deck.add_image_fit(slide, "总流程_Type分支.png", 0.55, 1.18, 12.2, 4.05) data = [ ["Type", "方向", "输出"], ["2001/2002/2003/2004/16", "文件生成或数据库文件读取", "二进制下载"], ["15", "文件上传并调用存储过程", "JSON 文本"], ["4000", "获取请求 IP", "文本 IP"], ["default", "DataLink.SqlWebCall 二次分发", "JSON 或空字符串"], ] deck.table(slide, 1.0, 5.45, 11.3, 1.25, data, col_widths=[2.7, 5.2, 3.4], font_size=8.5) deck.add_footer(slide) # 10 response main slide = deck.blank() deck.title(slide, "响应输出主流程图", "主图将四类响应出口分开:文件下载、上传 JSON、IP 文本、default JSON。") deck.add_image_fit(slide, "响应主_分支出口.png", 0.55, 1.15, 12.25, 5.75) deck.add_footer(slide) # 11 response rules slide = deck.blank() deck.title(slide, "响应规则与异常兜底", "迁移时要特别关注 Response.End、空响应和异常不记录的问题。") deck.add_image_fit(slide, "响应主_规则异常.png", 0.7, 1.22, 11.9, 3.2) deck.bullets( slide, 1.0, 4.75, 11.2, 1.55, [ "文件下载统一设置 application/octet-stream 和 Content-Disposition,前端通过响应头取文件名。", "外层 catch 只写 responseText,默认值为 NULL,不设置 HTTP 状态码。", "Type=16 若 bytes 为 null 直接 return,调用方可能收到空响应。", ], font_size=13.1, ) deck.add_footer(slide) # 12 section deck.section_slide("第二部分:SqlWebCall 默认分支", "说明普通业务请求进入 DataLink.SqlWebCall 后如何初始化、二次分发、解析参数、执行数据库并返回。") # 13 secondary full slide = deck.blank() deck.title(slide, "SqlWebCall 默认分支完整图", "完整图用于快速定位初始化、四类 Type、数据库执行和返回出口。") deck.add_image_fit(slide, "SqlWebCall_完整.png", 0.55, 1.1, 12.25, 5.9) deck.add_footer(slide) # 14 init slide = deck.blank() deck.title(slide, "SqlWebCall 初始化流程", "第一次进入时读取 Web.config 的 ConnectionString,并通过静态标记复用。") deck.add_image_fit(slide, "SqlWebCall_初始化.png", 0.75, 1.25, 7.0, 3.05) deck.bullets( slide, 8.1, 1.35, 4.45, 3.7, [ "InitSystemReg 当前直接读取 appSettings[\"ConnectionString\"]。", "原注册号、序列号、加密连接串校验逻辑已注释。", "移植后需确保 Web.config 中连接串名称和权限可用。", ], font_size=13, title="迁移关注点", ) deck.card(slide, 0.95, 5.35, 11.35, 0.75, "结论", "数据库连接配置是 SqlWebCall 能否工作的第一检查点;同时建议在移植阶段评估明文连接串和账号权限。", "red_light", "red", "red") deck.add_footer(slide) # 15 type secondary slide = deck.blank() deck.title(slide, "SqlWebCall Type 二次分发", "default 分支内部按 Type 再拆成四类:登录/加密、旧协议、新协议、SQL/命令。") deck.add_image_fit(slide, "SqlWebCall_Type二次分发.png", 0.55, 1.12, 12.25, 5.9) deck.add_footer(slide) # 16 type table slide = deck.blank() deck.title(slide, "SqlWebCall Type 能力表", "迁移测试时可按这些 Type 分组准备用例。") data = [ ["分类", "Type", "方法/能力", "迁移重点"], ["登录/加密", "8888/5001/5002", "加密、注册/改密、登录生成 token", "确认加密逻辑和 token 存储过程"], ["旧协议存储过程", "1/2/5", "jsonobj + 拼接字符串 Param", "兼容旧前端参数格式"], ["新协议存储过程", "11/111/12/13/21", "JsonData + JSON 数组字符串 Param", "确认存储过程名、参数、分页输出"], ["SQL/命令", "1001/1002/3/4/7/22/3001", "直接 SQL、建表导入、命令执行", "强烈建议白名单和权限隔离"], ] deck.table(slide, 0.55, 1.3, 12.25, 4.3, data, col_widths=[2.0, 2.3, 4.2, 3.75], font_size=8.2) deck.card(slide, 0.75, 6.0, 11.85, 0.65, "关键判断", "这部分是移植风险最大的区域:协议兼容、数据库权限、SQL 执行能力、token 逻辑都集中在 SqlWebCall 下游。", "amber_light", "amber", "amber") deck.add_footer(slide) # 17 param protocol slide = deck.blank() deck.title(slide, "参数协议:新协议与旧协议", "迁移时要避免把 Param 当作普通对象,它在新协议中通常是 JSON 数组字符串。") deck.card(slide, 0.7, 1.25, 5.9, 4.7, "新协议 Param", "适用:11/111/12/13/15/16/21/2004 等\n\n格式:\n[{\"name\":\"@p\",\"value\":\"v\",\"type\":\"string\"}]\n\n特点:\n- Param 字段本身是字符串\n- 支持 type 和 output\n- 数组 value 会转逗号字符串", "green_light", "green", "green") deck.card(slide, 6.95, 1.25, 5.65, 4.7, "旧协议 Param", "适用:1/2/5 等\n\nType 1/2:\n@p=value=type&@out=0=int=output\n\nType 5:\n@p&value&type|@p2&value&type\n\n特点:\n- 分隔符敏感\n- 值包含 &、=、| 时容易出错", "purple_light", "purple", "purple") deck.add_footer(slide) # 18 db and return slide = deck.blank() deck.title(slide, "数据库执行链路与返回格式", "SqlWebCall 只路由,真正执行在 DataLinkMesWork.cs 和 SQLCommon.cs。") deck.add_image_fit(slide, "SqlWebCall_数据库返回.png", 0.62, 1.15, 12.05, 4.0) deck.bullets( slide, 0.85, 5.45, 11.8, 1.1, [ "存储过程使用 ExecuteStoredProcedure,CommandType=StoredProcedure,CommandTimeout=0。", "SQL 查询使用 ExecuteSelectMesWork / ExecuteDataTable / ExecuteDataset。", "返回格式混合:DataTable JSON、result=1/0、rows-total、code-message-data、空字符串。", ], font_size=12.6, ) deck.add_footer(slide) # 19 risk slide = deck.blank() deck.title(slide, "风险与迁移控制点", "这些问题不一定阻塞移植,但会影响上线后的安全性和可维护性。") left = [ "token 多数是非空才校验;不传 token 的路径常见不拒绝。", "客户端可控制 Name/name,可能是存储过程名、SQL 文本或表名。", "Type=7 可按请求内容 DROP/CREATE 表。", "CommandTimeout=0,慢 SQL 或阻塞存储过程可能长期占用请求。", ] right = [ "下载异常和 bytes=null 缺少统一错误响应。", "外层 catch 不记录日志,排障成本高。", "明文连接串和数据库账号权限需要重新评估。", "响应格式不统一,前端和联调工具需按 Type 分别判断。", ] deck.card(slide, 0.75, 1.28, 5.85, 4.9, "SqlWebCall 风险", "\n".join(f"• {x}" for x in left), "red_light", "red", "red") deck.card(slide, 6.95, 1.28, 5.65, 4.9, "响应与部署风险", "\n".join(f"• {x}" for x in right), "amber_light", "amber", "amber") deck.add_footer(slide) # 20 section deck.section_slide("第三部分:项目移植资料", "给迁移实施人员使用:环境、配置、文件、数据库、接口验证和上线前检查。") # 21 migration checklist slide = deck.blank() deck.title(slide, "移植检查清单", "按部署前必须确认的对象拆分。") data = [ ["类别", "检查项", "说明"], ["运行环境", ".NET Framework 4.8 / IIS / ASP.NET", "WebHandler 运行环境需一致或验证兼容"], ["Web 文件", "MES_Manage/submit/MESCommonBase.ashx", "路径和 Class 名需保持"], ["DLL 依赖", "Bin 下 DataLinkMesWork、BasicData、WriteExcelNPOI 等", "版本差异会影响运行和导出"], ["配置", "Web.config / ConnectionString / CORS", "连接串、跨域、默认文档、handlers"], ["数据库", "存储过程、表、权限、token 过程", "SqlWebCall 能力依赖数据库对象"], ["文件导出", "NPOI、Spire、模板逻辑", "2001/2002/2003 验证"], ["安全", "token、白名单、日志、账号权限", "建议迁移阶段同步补强"], ] deck.table(slide, 0.55, 1.28, 12.25, 5.25, data, col_widths=[1.5, 4.4, 6.35], font_size=8.5) deck.add_footer(slide) # 22 migration steps slide = deck.blank() deck.title(slide, "建议移植实施顺序", "先让原协议跑通,再逐步补齐安全和可维护性。") steps = [ ("1", "环境准备", ".NET/IIS/站点路径/应用程序池"), ("2", "文件迁移", "ashx、Bin、Web.config、模板和静态资源"), ("3", "数据库迁移", "库表、存储过程、权限、token 相关过程"), ("4", "接口冒烟", "15/16/2001/2002/4000/default 常用 Type"), ("5", "风险补强", "鉴权、白名单、日志、连接串和权限"), ("6", "回归验收", "按 Type 分组准备测试矩阵和下载校验"), ] for i, (num, title, body) in enumerate(steps): x = 0.75 + (i % 3) * 4.05 y = 1.35 + (i // 3) * 2.35 deck.card(slide, x, y, 3.55, 1.65, f"{num}. {title}", body, "white", "blue" if i < 3 else "green", "navy") deck.card(slide, 1.1, 6.3, 11.1, 0.55, "执行原则", "先保持协议兼容,再处理风险收敛;不要在未完成回归测试前重构 Param 协议或响应格式。", "amber_light", "amber", "amber") deck.add_footer(slide) # 23 test matrix slide = deck.blank() deck.title(slide, "迁移验证用例矩阵", "每组至少准备一个成功、一个失败、一个边界场景。") data = [ ["场景", "覆盖 Type", "验证点"], ["Excel/PDF 导出", "2001/2002/2003", "文件名、扩展名、下载流、跨域响应头"], ["数据库文件下载", "16/2004", "bytes 为空、文件名、后缀、二进制一致性"], ["文件上传", "15", "无文件、有文件、文件名无扩展名、大文件"], ["IP 查询", "4000", "代理头、REMOTE_ADDR、UserHostAddress"], ["旧协议", "1/2/5", "Param 分隔符、输出参数、分页、token"], ["新协议", "11/111/12/13/21", "JSON 数组 Param、分页、DataSet、新结构返回"], ["SQL 执行", "1001/1002/3/4/7/22/3001", "SQL 参数、权限、白名单、错误处理"], ] deck.table(slide, 0.55, 1.25, 12.25, 5.45, data, col_widths=[2.35, 2.5, 7.4], font_size=8.3) deck.add_footer(slide) # 24 recommended improvements slide = deck.blank() deck.title(slide, "移植后的改造建议", "不建议在第一天全部重构,但建议列入上线前后计划。") deck.card(slide, 0.75, 1.25, 3.75, 4.95, "短期", "• 统一 token 鉴权\n• 未支持 Type 返回明确错误\n• 下载空文件返回可识别错误\n• 外层 catch 写日志\n• 修正 Type=4000 IP 逻辑", "green_light", "green", "green") deck.card(slide, 4.8, 1.25, 3.75, 4.95, "中期", "• 建立 Type + Name 白名单\n• Param 从字符串迁移为 JSON 数组\n• 统一 JSON 返回结构\n• SQL 执行能力与普通接口分离", "blue_light", "blue", "blue") deck.card(slide, 8.85, 1.25, 3.75, 4.95, "长期", "• 按业务模块拆分接口\n• 数据库账号最小权限\n• 移除明文连接串\n• 加入审计日志和限流", "purple_light", "purple", "purple") deck.add_footer(slide) # 25 appendix slide = deck.blank() deck.title(slide, "附录:资料追溯与后续维护", "PPT 是汇报和迁移视图,Markdown 与 Mermaid 是详细资料和可维护源。") deck.bullets( slide, 0.9, 1.35, 11.9, 4.6, [ "详细文字资料:MESCommonBase程序梳理.md、MESCommonBase响应输出与SqlWebCall默认分支详细文档.md。", "流程图源文件:MESCommonBase流程图.mmd、MESCommonBase响应输出与SqlWebCall主流程图.mmd、MESCommonBase响应输出与SqlWebCall次流程图.mmd。", "高清流程图:三张 PNG 已在 working 中保存,PPT 中按完整图和局部截图引用。", "自动生成脚本:render_*.py 与 build_mescommonbase_migration_ppt.py,可在文档更新后重新生成资料。", ], font_size=14, ) deck.card(slide, 1.1, 6.1, 11.1, 0.65, "输出文件", "working/MESCommonBase迁移分析资料包.pptx", "white", "blue", "blue") deck.add_footer(slide) deck.save() if __name__ == "__main__": build_deck() print(OUT)