# -*- coding: utf-8 -*- from pathlib import Path import math from PIL import Image, ImageDraw, ImageFont BASE_DIR = Path(__file__).resolve().parent OUT = BASE_DIR / "MESCommonBase流程图.png" W, H = 5200, 3800 img = Image.new("RGB", (W, H), "#F7F9FC") draw = ImageDraw.Draw(img) FONT_CANDIDATES = [ r"C:\Windows\Fonts\NotoSansSC-VF.ttf", r"C:\Windows\Fonts\simhei.ttf", r"C:\Windows\Fonts\Deng.ttf", r"C:\Windows\Fonts\simsun.ttc", ] def load_font(size, bold=False): candidates = [] if bold: candidates.extend([r"C:\Windows\Fonts\Dengb.ttf", r"C:\Windows\Fonts\simhei.ttf"]) candidates.extend(FONT_CANDIDATES) for path in candidates: try: return ImageFont.truetype(path, size=size) except Exception: pass return ImageFont.load_default() font_title = load_font(72, True) font_subtitle = load_font(34) font_box = load_font(31) font_box_small = load_font(27) font_panel_title = load_font(42, True) font_note = load_font(28) font_edge = load_font(26) def text_size(text, font): bbox = draw.textbbox((0, 0), text, font=font) return bbox[2] - bbox[0], bbox[3] - bbox[1] def wrap_line(line, font, max_width): if not line: return [""] lines = [] current = "" for ch in line: test = current + ch if text_size(test, font)[0] <= max_width or not current: current = test else: lines.append(current) current = ch if current: lines.append(current) return lines def wrap_text(text, font, max_width): lines = [] for raw in text.split("\n"): lines.extend(wrap_line(raw, font, max_width)) return lines def draw_multiline_center(text, cx, cy, max_width, font, fill="#172033", line_gap=12): lines = wrap_text(text, font, max_width) heights = [text_size(line, font)[1] for line in lines] total_h = sum(heights) + line_gap * (len(lines) - 1) y = cy - total_h / 2 for line, h in zip(lines, heights): width, _ = text_size(line, font) draw.text((cx - width / 2, y), line, font=font, fill=fill) y += h + line_gap def draw_multiline_left(text, x, y, max_width, font, fill="#172033", line_gap=10): lines = wrap_text(text, font, max_width) yy = y for line in lines: draw.text((x, yy), line, font=font, fill=fill) yy += text_size(line, font)[1] + line_gap return yy def box_bounds(cx, cy, w, h): return (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2) def draw_box(cx, cy, w, h, text, fill="#FFFFFF", outline="#2E5A8A", width=5, font=None, radius=34): font = font or font_box bounds = box_bounds(cx, cy, w, h) draw.rounded_rectangle(bounds, radius=radius, fill=fill, outline=outline, width=width) draw_multiline_center(text, cx, cy, w - 70, font) return bounds def draw_diamond(cx, cy, w, h, text, fill="#FFF4CC", outline="#A66F00", width=5, font=None): font = font or font_box points = [(cx, cy - h / 2), (cx + w / 2, cy), (cx, cy + h / 2), (cx - w / 2, cy)] draw.polygon(points, fill=fill, outline=outline) for i in range(width): offset = i * 0.9 border_points = [ (cx, cy - h / 2 + offset), (cx + w / 2 - offset, cy), (cx, cy + h / 2 - offset), (cx - w / 2 + offset, cy), ] draw.line(border_points + [border_points[0]], fill=outline, width=1) draw_multiline_center(text, cx, cy, w - 80, font) return points def draw_panel(x, y, w, h, title, fill="#FFFFFF", outline="#B9C4D6"): draw.rounded_rectangle((x, y, x + w, y + h), radius=34, fill=fill, outline=outline, width=5) draw.text((x + 45, y + 28), title, font=font_panel_title, fill="#172033") def arrow(points, color="#344054", width=6, label=None, label_pos=None): for p1, p2 in zip(points, points[1:]): draw.line((p1[0], p1[1], p2[0], p2[1]), fill=color, width=width) x1, y1 = points[-2] x2, y2 = points[-1] angle = math.atan2(y2 - y1, x2 - x1) size = 24 left = (x2 - size * math.cos(angle - math.pi / 7), y2 - size * math.sin(angle - math.pi / 7)) right = (x2 - size * math.cos(angle + math.pi / 7), y2 - size * math.sin(angle + math.pi / 7)) draw.polygon([(x2, y2), left, right], fill=color) if label: lx, ly = label_pos if label_pos else ((x1 + x2) / 2, (y1 + y2) / 2) tw, th = text_size(label, font_edge) draw.rounded_rectangle( (lx - tw / 2 - 14, ly - th / 2 - 8, lx + tw / 2 + 14, ly + th / 2 + 8), radius=12, fill="#F7F9FC", ) draw.text((lx - tw / 2, ly - th / 2), label, font=font_edge, fill=color) def draw_title(): main_title = "MESCommonBase.ashx 主流程图" subtitle = "入口解析 -> Type 分发 -> DataLink/ExcelWebCall 执行 -> 响应输出" tw, _ = text_size(main_title, font_title) draw.text(((W - tw) / 2, 45), main_title, font=font_title, fill="#111827") tw, _ = text_size(subtitle, font_subtitle) draw.text(((W - tw) / 2, 132), subtitle, font=font_subtitle, fill="#475467") def draw_top_flow(): cx = W / 2 draw_box(cx, 285, 900, 125, "HTTP 请求\nMES_Manage/submit/MESCommonBase.ashx", "#E8F1FF", "#2864B4", font=font_box) draw_box(cx, 475, 1320, 145, "ProcessRequest\n设置 application/json,读取 Request.InputStream", "#E8F1FF", "#2864B4", font=font_box) draw_box(cx, 670, 1320, 145, "JsonMapper.ToObject(stream)\n尝试读取 Type / type", "#EAF7EF", "#227A45", font=font_box) draw_box(cx, 865, 1520, 145, "JavaScriptSerializer.Deserialize(stream)\n成功后用 dataobj.Type 覆盖 type", "#EAF7EF", "#227A45", font=font_box) draw_diamond(cx, 1090, 520, 190, "dataobj\n== null?", "#FFF4CC", "#B27700", font=font_box) draw_box(3890, 1090, 840, 130, '读取 Request["param"]\n再次解析 Type / type', "#FFF7E6", "#B27700", font=font_box_small) draw_diamond(cx, 1315, 500, 180, "switch\n(type)", "#FFF4CC", "#B27700", font=font_box) arrow([(cx, 348), (cx, 402)]) arrow([(cx, 548), (cx, 598)]) arrow([(cx, 742), (cx, 792)]) arrow([(cx, 938), (cx, 995)]) arrow([(cx + 260, 1090), (3470, 1090)], label="是", label_pos=(3140, 1050)) arrow([(3890, 1155), (3890, 1240), (2850, 1240), (2850, 1315)]) arrow([(cx, 1185), (cx, 1225)], label="否", label_pos=(2555, 1210)) def draw_branch_panel(): panel_x, panel_y, panel_w, panel_h = 160, 1460, 4880, 1120 draw_panel(panel_x, panel_y, panel_w, panel_h, "Type 分发分支", fill="#FFFFFF", outline="#C7D2E5") arrow([(W / 2, 1405), (W / 2, panel_y + 10)]) card_w, card_h = 1080, 330 xs = [790, 1960, 3130, 4300] y1, y2 = 1740, 2205 cards = [ (xs[0], y1, "2001 Excel 导出\nExcelWebCall.ExcelFile(jsonData)\n生成 bytes / fileName / extension\n响应:二进制下载", "#EAF7EF", "#227A45"), (xs[1], y1, "2002 PDF 导出\nExcelWebCall.ExcelFilePdf(jsonData)\nExcel 转 PDF\n响应:二进制下载", "#EAF7EF", "#227A45"), (xs[2], y1, "2003 合成 Excel 图片/扩展名导出\n读取 Form 第一项 dataimg[0]\nExcelWebCall.ExcelFile(jsonData)\n响应:二进制下载", "#EAF7EF", "#227A45"), (xs[3], y1, "2004 数据库文件下载\nDataLink.ExePROCEDURE_Type2004\n返回 bytes / fileName / extension\n响应:二进制下载", "#EAF7EF", "#227A45"), (xs[0], y2, "15 文件上传\n读取 Request.Files[0]\n拆分 name / suffix / bytes\nDataLink.ExePROCEDURE_Type15\n响应:JSON", "#EEF2FF", "#4F46E5"), (xs[1], y2, "16 数据库文件下载\nDataLink.ExePROCEDURE_Type16\n返回 fileName / suffix / bytes\nbytes 为 null 时直接 return", "#EAF7EF", "#227A45"), (xs[2], y2, "4000 IP 查询\n读取 ServerVariables / UserHostAddress\n响应:文本 IP", "#FFF7E6", "#B27700"), (xs[3], y2, "default 通用业务调用\nDataLink.SqlWebCall(type,jsonData,dataobj)\n进入数据库/存储过程二次分发", "#F3E8FF", "#7E22CE"), ] for cx, cy, text, fill, outline in cards: draw_box(cx, cy, card_w, card_h, text, fill, outline, font=font_box_small, radius=28) return panel_x, panel_y, panel_w, panel_h, xs, y2, card_h def draw_detail_panels(panel_info): panel_x, panel_y, panel_w, panel_h, xs, y2, card_h = panel_info resp_x, resp_y, resp_w, resp_h = 260, 2740, 2050, 740 detail_x, detail_y, detail_w, detail_h = 2520, 2740, 2420, 740 draw_panel(resp_x, resp_y, resp_w, resp_h, "响应输出", fill="#FFFFFF", outline="#C7D2E5") resp_text = ( "文件分支 2001/2002/2003/2004/16:\n" " application/octet-stream + Content-Disposition\n" " BinaryWrite(bytes) + Flush/End\n\n" "Type 15 与 default:\n" " Response.Write(JSON 文本)\n\n" "Type 4000:\n" " Response.Write(userIP)\n\n" "外层 catch:异常时写当前 responseText,初始值为 NULL" ) draw_multiline_left(resp_text, resp_x + 60, resp_y + 115, resp_w - 120, font_note, fill="#344054", line_gap=12) draw_panel(detail_x, detail_y, detail_w, detail_h, "DataLink.SqlWebCall 默认分支", fill="#FFFFFF", outline="#C7D2E5") detail_text = ( "初始化:InitSystemReg 读取 Web.config 的 ConnectionString\n\n" "8888 / 5001 / 5002:加密、注册/改密、登录并生成 token\n\n" "1 / 2 / 5:旧协议存储过程,Param 为拼接字符串\n\n" "11 / 111 / 12 / 13 / 21:新协议存储过程,Param 为 JSON 数组字符串\n\n" "1001 / 1002 / 3 / 4 / 7 / 22 / 3001:直接 SQL、建表导入或 SQL 命令执行\n\n" "输出:数组 JSON 或 { code, message, data }" ) draw_multiline_left(detail_text, detail_x + 60, detail_y + 115, detail_w - 120, font_note, fill="#344054", line_gap=12) arrow([(xs[3], y2 + card_h / 2), (xs[3], 2630), (detail_x + detail_w / 2, 2630), (detail_x + detail_w / 2, detail_y)], color="#7E22CE") arrow([(panel_x + 1150, panel_y + panel_h), (panel_x + 1150, resp_y)], color="#227A45") arrow([(detail_x, detail_y + detail_h / 2), (resp_x + resp_w, detail_y + detail_h / 2)], color="#7E22CE") return resp_x, resp_y, resp_w, resp_h def draw_note_and_end(resp_panel): resp_x, resp_y, resp_w, resp_h = resp_panel note_x, note_y, note_w, note_h = 520, 3545, 4160, 150 draw.rounded_rectangle((note_x, note_y, note_x + note_w, note_y + note_h), radius=30, fill="#FFF1F3", outline="#C01048", width=5) note = "关键注意:入口层缺少统一鉴权和白名单;客户端可通过 Type + Name/Param 触发 SQL 或存储过程能力,异常多数不记录日志。" draw_multiline_center(note, note_x + note_w / 2, note_y + note_h / 2, note_w - 120, font_note, fill="#7A271A", line_gap=8) end_x, end_y = W - 560, H - 125 draw_box(end_x, end_y, 420, 95, "请求结束", "#E8F1FF", "#2864B4", font=font_box_small, radius=45) arrow([(resp_x + resp_w / 2, resp_y + resp_h), (resp_x + resp_w / 2, H - 125), (end_x - 210, H - 125)], color="#344054") footer = "源文件:working/MESCommonBase流程图.mmd PNG:working/MESCommonBase流程图.png" fw, _ = text_size(footer, font_edge) draw.text(((W - fw) / 2, H - 45), footer, font=font_edge, fill="#667085") draw_title() draw_top_flow() panel_info = draw_branch_panel() resp_panel = draw_detail_panels(panel_info) draw_note_and_end(resp_panel) img.save(OUT, format="PNG", dpi=(220, 220), optimize=True) print(OUT) print(f"{W}x{H}")