323 lines
16 KiB
Python
323 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
from pathlib import Path
|
||
import math
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
|
||
BASE_DIR = Path(__file__).resolve().parent
|
||
|
||
|
||
FONT_CANDIDATES = [
|
||
r"C:\Windows\Fonts\simhei.ttf",
|
||
r"C:\Windows\Fonts\Dengb.ttf",
|
||
r"C:\Windows\Fonts\NotoSansSC-VF.ttf",
|
||
r"C:\Windows\Fonts\Deng.ttf",
|
||
r"C:\Windows\Fonts\simsun.ttc",
|
||
]
|
||
|
||
|
||
def make_canvas(width, height):
|
||
img = Image.new("RGB", (width, height), "#F7F9FC")
|
||
draw = ImageDraw.Draw(img)
|
||
return img, draw
|
||
|
||
|
||
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()
|
||
|
||
|
||
class Diagram:
|
||
def __init__(self, width, height):
|
||
self.width = width
|
||
self.height = height
|
||
self.img, self.draw = make_canvas(width, height)
|
||
self.font_title = load_font(72, True)
|
||
self.font_subtitle = load_font(34)
|
||
self.font_section = load_font(40, True)
|
||
self.font_box = load_font(33)
|
||
self.font_small = load_font(29)
|
||
self.font_note = load_font(31)
|
||
self.font_edge = load_font(24)
|
||
|
||
def text_size(self, text, font):
|
||
bbox = self.draw.textbbox((0, 0), text, font=font)
|
||
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||
|
||
def wrap_line(self, line, font, max_width):
|
||
if not line:
|
||
return [""]
|
||
lines = []
|
||
current = ""
|
||
for ch in line:
|
||
test = current + ch
|
||
if self.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(self, text, font, max_width):
|
||
lines = []
|
||
for raw in text.split("\n"):
|
||
lines.extend(self.wrap_line(raw, font, max_width))
|
||
return lines
|
||
|
||
def text_center(self, text, cx, cy, max_width, font=None, fill="#172033", line_gap=10):
|
||
font = font or self.font_box
|
||
lines = self.wrap_text(text, font, max_width)
|
||
heights = [self.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):
|
||
w, _ = self.text_size(line, font)
|
||
self.draw.text((cx - w / 2, y), line, font=font, fill=fill)
|
||
y += h + line_gap
|
||
|
||
def text_left(self, text, x, y, max_width, font=None, fill="#344054", line_gap=10):
|
||
font = font or self.font_note
|
||
yy = y
|
||
for line in self.wrap_text(text, font, max_width):
|
||
self.draw.text((x, yy), line, font=font, fill=fill)
|
||
yy += self.text_size(line, font)[1] + line_gap
|
||
return yy
|
||
|
||
def title(self, title, subtitle):
|
||
tw, _ = self.text_size(title, self.font_title)
|
||
self.draw.text(((self.width - tw) / 2, 50), title, font=self.font_title, fill="#111827")
|
||
sw, _ = self.text_size(subtitle, self.font_subtitle)
|
||
self.draw.text(((self.width - sw) / 2, 138), subtitle, font=self.font_subtitle, fill="#475467")
|
||
|
||
def box(self, cx, cy, w, h, text, fill="#FFFFFF", outline="#2E5A8A", radius=28, width=5, font=None):
|
||
font = font or self.font_box
|
||
bounds = (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2)
|
||
self.draw.rounded_rectangle(bounds, radius=radius, fill=fill, outline=outline, width=width)
|
||
self.text_center(text, cx, cy, w - 60, font=font)
|
||
return bounds
|
||
|
||
def panel(self, x, y, w, h, title, fill="#FFFFFF", outline="#C7D2E5"):
|
||
self.draw.rounded_rectangle((x, y, x + w, y + h), radius=34, fill=fill, outline=outline, width=5)
|
||
self.draw.text((x + 38, y + 26), title, font=self.font_section, fill="#172033")
|
||
|
||
def diamond(self, cx, cy, w, h, text, fill="#FFF4CC", outline="#B27700"):
|
||
points = [(cx, cy - h / 2), (cx + w / 2, cy), (cx, cy + h / 2), (cx - w / 2, cy)]
|
||
self.draw.polygon(points, fill=fill, outline=outline)
|
||
self.draw.line(points + [points[0]], fill=outline, width=5)
|
||
self.text_center(text, cx, cy, w - 70, font=self.font_box)
|
||
return points
|
||
|
||
def arrow(self, points, color="#344054", width=6, label=None, label_pos=None):
|
||
for p1, p2 in zip(points, points[1:]):
|
||
self.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))
|
||
self.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 = self.text_size(label, self.font_edge)
|
||
self.draw.rounded_rectangle(
|
||
(lx - tw / 2 - 12, ly - th / 2 - 7, lx + tw / 2 + 12, ly + th / 2 + 7),
|
||
radius=10,
|
||
fill="#F7F9FC",
|
||
)
|
||
self.draw.text((lx - tw / 2, ly - th / 2), label, font=self.font_edge, fill=color)
|
||
|
||
def note_box(self, x, y, w, h, text, fill="#FFF1F3", outline="#C01048"):
|
||
self.draw.rounded_rectangle((x, y, x + w, y + h), radius=28, fill=fill, outline=outline, width=5)
|
||
self.text_center(text, x + w / 2, y + h / 2, w - 90, font=self.font_note, fill="#7A271A")
|
||
|
||
def footer(self, text):
|
||
tw, _ = self.text_size(text, self.font_edge)
|
||
self.draw.text(((self.width - tw) / 2, self.height - 52), text, font=self.font_edge, fill="#667085")
|
||
|
||
def save(self, name):
|
||
path = BASE_DIR / name
|
||
self.img.save(path, format="PNG", dpi=(220, 220), optimize=True)
|
||
print(f"{path} {self.width}x{self.height}")
|
||
|
||
|
||
def draw_main():
|
||
d = Diagram(5400, 3400)
|
||
d.title("MESCommonBase 响应输出与 SqlWebCall 主流程图", "主图:入口 type 分支如何进入二进制下载、JSON、IP 文本和 SqlWebCall")
|
||
|
||
cx = d.width / 2
|
||
d.box(cx, 290, 1050, 125, "MESCommonBase.ashx\nProcessRequest", "#E8F1FF", "#2864B4")
|
||
d.box(cx, 480, 1400, 140, "解析请求并取得 type\nJSON body / jsonobj / Request[\"param\"]", "#EAF7EF", "#227A45")
|
||
d.diamond(cx, 690, 480, 170, "switch\n(type)")
|
||
d.arrow([(cx, 352), (cx, 410)])
|
||
d.arrow([(cx, 550), (cx, 607)])
|
||
|
||
panel_x, panel_y, panel_w, panel_h = 190, 860, 5020, 1120
|
||
d.panel(panel_x, panel_y, panel_w, panel_h, "1. Type 首层分支")
|
||
d.arrow([(cx, 775), (cx, panel_y + 5)])
|
||
|
||
lanes = [
|
||
(830, "2001/2002/2003/2004/16\n文件下载类分支\n生成或读取 bytes / fileName / extension", "#EAF7EF", "#227A45"),
|
||
(2040, "15\n文件上传分支\n读取 Request.Files[0]\n调用 ExePROCEDURE_Type15", "#EEF2FF", "#4F46E5"),
|
||
(3250, "4000\nIP 查询分支\n读取 ServerVariables / UserHostAddress", "#FFF7E6", "#B27700"),
|
||
(4460, "default\n普通业务分支\nDataLink.SqlWebCall(type,jsonData,dataobj)", "#F3E8FF", "#7E22CE"),
|
||
]
|
||
for x, text, fill, outline in lanes:
|
||
d.box(x, 1110, 1080, 250, text, fill, outline, font=d.font_small)
|
||
|
||
d.diamond(830, 1450, 430, 150, "bytes\n有效?", "#FFF4CC", "#B27700")
|
||
d.box(830, 1760, 1080, 250, "二进制下载响应\napplication/octet-stream\nContent-Disposition: attachment\nBinaryWrite + Flush + End/Close", "#EAF7EF", "#227A45", font=d.font_small)
|
||
d.box(1320, 1545, 480, 130, "无文件\n直接 return\n可能空响应", "#FFF1F3", "#C01048", font=d.font_small)
|
||
d.arrow([(830, 1235), (830, 1375)])
|
||
d.arrow([(830, 1525), (830, 1635)], label="是", label_pos=(780, 1580))
|
||
d.arrow([(1045, 1450), (1165, 1450), (1165, 1545), (1080, 1545)], label="否", label_pos=(1145, 1408))
|
||
|
||
d.box(2040, 1760, 1080, 250, "JSON 文本响应\nResponse.Write(responseText)\n常见 result=1 / result=0", "#EEF2FF", "#4F46E5", font=d.font_small)
|
||
d.arrow([(2040, 1235), (2040, 1635)])
|
||
|
||
d.box(3250, 1760, 1080, 250, "文本 IP 响应\nResponse.Write(userIP)\nContent-Type 仍可能是 application/json", "#FFF7E6", "#B27700", font=d.font_small)
|
||
d.arrow([(3250, 1235), (3250, 1635)])
|
||
|
||
d.box(4460, 1450, 1080, 220, "SqlWebCall 返回字符串\n数组 JSON / code-message-data\n未支持 Type 可能为空字符串", "#F3E8FF", "#7E22CE", font=d.font_small)
|
||
d.arrow([(4460, 1235), (4460, 1340)])
|
||
d.box(4460, 1760, 1080, 250, "JSON 文本响应\nHeaders.Remove(\"Server\")\nResponse.Write(responseText)", "#EEF2FF", "#4F46E5", font=d.font_small)
|
||
d.arrow([(4460, 1560), (4460, 1635)])
|
||
|
||
d.panel(360, 2140, 2280, 660, "2. 响应出口规则")
|
||
d.text_left(
|
||
"文件下载:设置 application/octet-stream,写 Content-Disposition,BinaryWrite(bytes)。\n\n"
|
||
"上传/default:使用 Response.Write 写 JSON 字符串,格式由下游决定。\n\n"
|
||
"IP 查询:直接写 userIP,实际是文本。\n\n"
|
||
"Type=16 bytes 为 null 时直接 return,调用方可能收到空响应。",
|
||
430,
|
||
2245,
|
||
2140,
|
||
font=d.font_note,
|
||
)
|
||
|
||
d.panel(2860, 2140, 2180, 660, "3. 外层异常兜底")
|
||
d.text_left(
|
||
"responseText 初始值为 NULL。\n\n"
|
||
"ProcessRequest 外层 catch(Exception err) 不记录日志、不改 HTTP 状态码,只写当前 responseText。\n\n"
|
||
"如果异常发生在下游调用前,通常返回 NULL;如果发生在赋值后,可能返回旧结果。",
|
||
2930,
|
||
2245,
|
||
2040,
|
||
font=d.font_note,
|
||
)
|
||
|
||
d.note_box(
|
||
680,
|
||
2940,
|
||
4040,
|
||
150,
|
||
"主流程风险:响应格式混合(二进制/JSON/文本/NULL/空响应),Response.End 位于 try 内,default 分支执行能力取决于 SqlWebCall 内部 Type。",
|
||
)
|
||
d.box(d.width - 520, 3180, 420, 92, "请求结束", "#E8F1FF", "#2864B4", radius=45, font=d.font_small)
|
||
d.arrow([(d.width / 2, 3090), (d.width - 730, 3180)])
|
||
d.footer("源:MESCommonBase响应输出与SqlWebCall主流程图.mmd 图:MESCommonBase响应输出与SqlWebCall主流程图.png")
|
||
d.save("MESCommonBase响应输出与SqlWebCall主流程图.png")
|
||
|
||
|
||
def draw_secondary():
|
||
d = Diagram(5800, 4300)
|
||
d.title("DataLink.SqlWebCall 默认分支次流程图", "次图:SqlWebCall 内部初始化、Type 二次分发、参数解析、数据库执行和返回格式")
|
||
|
||
cx = d.width / 2
|
||
d.box(cx, 285, 1260, 120, "MESCommonBase default 分支\nDataLink.SqlWebCall(type,jsonData,dataobj)", "#F3E8FF", "#7E22CE")
|
||
d.diamond(cx, 500, 520, 160, "initSystemIsOk?", "#FFF4CC", "#B27700")
|
||
d.box(4150, 500, 1040, 140, "InitSystemReg\n读取 Web.config ConnectionString\ninitSystemIsOk = true", "#E8F1FF", "#2864B4", font=d.font_small)
|
||
d.diamond(cx, 735, 500, 160, "switch\n(type)", "#FFF4CC", "#B27700")
|
||
d.arrow([(cx, 345), (cx, 420)])
|
||
d.arrow([(cx + 260, 500), (3630, 500)], label="否", label_pos=(3430, 458))
|
||
d.arrow([(4150, 570), (4150, 735), (3150, 735)])
|
||
d.arrow([(cx, 580), (cx, 655)], label="是", label_pos=(2820, 625))
|
||
|
||
panel_y = 930
|
||
d.panel(150, panel_y, 5500, 1630, "1. Type 二次分发与处理")
|
||
|
||
cols = [
|
||
(860, "登录/加密类\n8888 / 5001 / 5002", "#E8F1FF", "#2864B4"),
|
||
(2180, "旧协议存储过程\n1 / 2 / 5", "#EEF2FF", "#4F46E5"),
|
||
(3500, "新协议存储过程\n11 / 111 / 12 / 13 / 21", "#EAF7EF", "#227A45"),
|
||
(4820, "SQL/命令执行类\n1001 / 1002 / 3 / 4 / 7 / 22 / 3001", "#FFF7E6", "#B27700"),
|
||
]
|
||
for x, header, fill, outline in cols:
|
||
d.box(x, 1140, 1180, 180, header, fill, outline, font=d.font_small)
|
||
|
||
d.box(860, 1420, 1180, 290, "8888:读取 Name/name 并加密\n5001:Password MD5 后执行存储过程\n5002:解密密码、比对数据库密码、成功生成 token", "#E8F1FF", "#2864B4", font=d.font_small)
|
||
d.box(860, 1805, 1180, 300, "返回格式\n加密字符串\n用户表 JSON + token\n失败 result=0,msg=...", "#FFFFFF", "#2864B4", font=d.font_small)
|
||
d.arrow([(860, 1230), (860, 1275)])
|
||
d.arrow([(860, 1565), (860, 1655)])
|
||
|
||
d.box(2180, 1420, 1180, 290, "输入对象:jsonobj dataobj\nParam 拼接字符串\nType 1/2: @p=value=type\nType 5: @p&value&type|...", "#EEF2FF", "#4F46E5", font=d.font_small)
|
||
d.box(2180, 1805, 1180, 300, "SQLCommon.GetCmdParam\n按 & / = / | 拆参数\n支持 output 参数\n随后 ExecuteStoredProcedure", "#FFFFFF", "#4F46E5", font=d.font_small)
|
||
d.arrow([(2180, 1230), (2180, 1275)])
|
||
d.arrow([(2180, 1565), (2180, 1655)])
|
||
|
||
d.box(3500, 1420, 1180, 290, "GetString_JsonData\n读取 Type/Name/Param/token\nParam 是 JSON 数组字符串\n兼容大小写字段", "#EAF7EF", "#227A45", font=d.font_small)
|
||
d.diamond(3500, 1785, 440, 150, "token\n非空?", "#FFF4CC", "#B27700")
|
||
d.box(3030, 2045, 470, 150, "CheckToken\n失败返回\nresult=3", "#FFF1F3", "#C01048", font=d.font_small)
|
||
d.box(3970, 2045, 650, 150, "Param 转 SqlParameter[]\n数组值转逗号字符串\noutput=1 设输出参数", "#EAF7EF", "#227A45", font=d.font_small)
|
||
d.arrow([(3500, 1230), (3500, 1275)])
|
||
d.arrow([(3500, 1565), (3500, 1710)])
|
||
d.arrow([(3280, 1785), (3030, 1970)], label="是", label_pos=(3195, 1890))
|
||
d.arrow([(3720, 1785), (3970, 1970)], label="否/通过", label_pos=(3880, 1890))
|
||
|
||
d.box(4820, 1420, 1180, 290, "Name/name 可能是 SQL 文本或表名\n1001/1002:带参数 SQL\n3/4/22:直接 SQL\n7:DROP/CREATE 表并导入", "#FFF7E6", "#B27700", font=d.font_small)
|
||
d.box(4820, 1805, 1180, 300, "SQLCommon.ExecuteDataTable / ExecuteDataset\nExecuteInsertMesWork / ExecuteSelectMesWork\n或 DbCallType1003_SqlCmd.SqlExec", "#FFFFFF", "#B27700", font=d.font_small)
|
||
d.arrow([(4820, 1230), (4820, 1275)])
|
||
d.arrow([(4820, 1565), (4820, 1655)])
|
||
|
||
d.panel(300, 2740, 2440, 780, "2. 底层数据库执行链路")
|
||
d.text_left(
|
||
"存储过程:SQLCommon.ExecuteStoredProcedure\n- SqlCommand.CommandType = StoredProcedure\n- CommandTimeout = 0\n- SqlDataAdapter.Fill(DataSet/DataTable)\n\n"
|
||
"SQL 查询:ExecuteSelectMesWork / ExecuteDataTable / ExecuteDataset\n- SqlDataAdapter(sql, conn)\n- Fill(dt/ds)\n\n"
|
||
"SQL 增删改:ExecuteInsertMesWork / ExecuteNonQuery\n- SqlCommand.CommandText = 请求传入 SQL\n- ExecuteNonQuery / ExecuteScalar",
|
||
380,
|
||
2845,
|
||
2290,
|
||
font=d.font_note,
|
||
)
|
||
|
||
d.panel(3060, 2740, 2440, 780, "3. 返回格式与响应出口")
|
||
d.text_left(
|
||
"DataTable JSON:Type 1/3/5/11/1002 常见。\n\n"
|
||
"result 标志:Type 2/4/12/1001 常见,成功 result=1,失败 result=0。\n\n"
|
||
"分页:Type 111 返回 { rows, total },total 来自 ItemCount 输出参数。\n\n"
|
||
"新结构:Type 21/22 返回 { code, message, data }。\n\n"
|
||
"未支持 Type:SqlWebCall 可能返回空字符串。",
|
||
3140,
|
||
2845,
|
||
2290,
|
||
font=d.font_note,
|
||
)
|
||
|
||
d.note_box(
|
||
560,
|
||
3700,
|
||
4680,
|
||
210,
|
||
"次流程风险:token 多数是非空才校验;客户端可控制 Name/name;SQL 文本、存储过程名、表名缺少统一白名单;CommandTimeout=0 可能导致长时间阻塞。",
|
||
)
|
||
d.box(d.width - 620, 4080, 560, 95, "返回 MESCommonBase\nResponse.Write", "#E8F1FF", "#2864B4", radius=45, font=d.font_small)
|
||
d.arrow([(d.width / 2, 3910), (d.width - 900, 4080)])
|
||
d.footer("源:MESCommonBase响应输出与SqlWebCall次流程图.mmd 图:MESCommonBase响应输出与SqlWebCall次流程图.png")
|
||
d.save("MESCommonBase响应输出与SqlWebCall次流程图.png")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
draw_main()
|
||
draw_secondary()
|