chore: initialize migration workspace

This commit is contained in:
2026-07-02 10:38:13 +08:00
commit 705aec6ab2
44 changed files with 5988 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
%% MESCommonBase 响应输出与 SqlWebCall 主流程图
flowchart TD
A([MESCommonBase.ashx<br/>ProcessRequest])
B[解析请求并取得 type<br/>JSON body / jsonobj / Request param]
C{switch(type)}
A --> B --> C
C -->|2001/2002/2003/2004/16| D[文件下载类分支<br/>生成或读取 bytes/fileName/extension]
C -->|15| E[文件上传分支<br/>读取 Request.Files[0]<br/>调用 ExePROCEDURE_Type15]
C -->|4000| F[IP 查询分支<br/>读取 ServerVariables / UserHostAddress]
C -->|default| G[普通业务分支<br/>DataLink.SqlWebCall(type,jsonData,dataobj)]
D --> D1{bytes 是否有效?}
D1 -->|是| R1[二进制下载响应<br/>Content-Type: application/octet-stream<br/>Content-Disposition: attachment<br/>BinaryWrite + Flush + End/Close]
D1 -->|否<br/>主要见 Type=16| R0[直接 return<br/>可能为空响应]
E --> R2[JSON 文本响应<br/>Response.Write(responseText)<br/>常见 result=1 / result=0]
F --> R3[文本 IP 响应<br/>Response.Write(userIP)<br/>Content-Type 仍可能是 application/json]
G --> G1[SqlWebCall 返回字符串<br/>数组 JSON / code-message-data / 空字符串]
G1 --> R2
A -.外层 catch(Exception err).-> X[异常兜底<br/>不记录异常<br/>Response.Write(responseText)<br/>初始 responseText = NULL]
X --> END([请求结束])
R1 --> END
R0 --> END
R2 --> END
R3 --> END
subgraph WARN[主流程注意点]
W1[Response.End 位于 try 内<br/>可能触发 ThreadAbortException]
W2[响应格式不统一<br/>二进制 / JSON / 文本 / NULL / 空响应]
W3[default 分支执行能力强<br/>依赖 SqlWebCall 内部 Type 二次分发]
end
END --> WARN

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

@@ -0,0 +1,53 @@
%% DataLink.SqlWebCall 默认分支次流程图
flowchart TD
A([MESCommonBase default 分支])
B[DataLink.SqlWebCall(type,jsonData,dataobj)]
C{initSystemIsOk?}
D[InitSystemReg<br/>读取 Web.config appSettings ConnectionString<br/>initSystemIsOk = true]
E{switch(type)}
A --> B --> C
C -->|否| D --> E
C -->|是| E
E -->|8888| L1[GetEncryptStr<br/>读取 Name/name 并加密]
E -->|5001| L2[ExePROCEDURE_Type11_AddUser<br/>Password MD5<br/>执行 Name 指定存储过程]
E -->|5002| L3[ExePROCEDURE_Type11_Login<br/>解密密码并比对<br/>成功写入 token]
E -->|1/2/5| O1[旧协议存储过程<br/>对象: jsonobj dataobj<br/>Param 为拼接字符串]
O1 --> O2[SQLCommon.GetCmdParam<br/>按 & / = / | 拆参数<br/>支持 output 参数]
O2 --> DBP[SQLCommon.ExecuteStoredProcedure]
E -->|11/111/12/13/21| N1[新协议存储过程<br/>对象: JsonData jsonData<br/>Param 为 JSON 数组字符串]
N1 --> N2[GetString_JsonData<br/>读取 Type/Name/Param/token/pageSize/pageList]
N2 --> T{token 非空?}
T -->|是| T1[CheckToken<br/>权限管理_Token_查询数据]
T -->|否| N3[解析 Param 为 SqlParameter[]]
T1 -->|通过| N3
T1 -->|失败| R3[返回 result=3]
N3 --> N4{Type=111 且有分页字段?}
N4 -->|是| N5[追加 PageCurrent/PageSize/PageCount/ItemCount]
N4 -->|否| DBP
N5 --> DBP
E -->|1001/1002/3/4/7/22/3001| S1[SQL / 建表导入 / 命令执行类<br/>Name/name 可能是 SQL 文本或表名]
S1 --> DBS[SQLCommon.ExecuteDataTable / ExecuteDataset<br/>ExecuteInsertMesWork / ExecuteSelectMesWork<br/>或 DbCallType1003_SqlCmd.SqlExec]
L1 --> R1[返回加密字符串]
L2 --> DBP
L3 --> DBP
DBP --> R2[返回结果<br/>DataTable JSON / DataSet JSON<br/>result=1/0 / rows-total / code-message-data]
DBS --> R2
R1 --> Z[Response.Write(responseText)]
R2 --> Z
R3 --> Z
Z --> END([返回 MESCommonBase default 响应出口])
subgraph RISK[次流程关键风险]
K1[token 多数是非空才校验<br/>不传 token 常见路径不拒绝]
K2[客户端可控制 Name/name<br/>存储过程名 / SQL 文本 / 表名]
K3[CommandTimeout = 0<br/>长 SQL 或存储过程可能长期阻塞]
K4[未支持 Type 返回空字符串<br/>异常常返回 result=0]
end
END --> RISK

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

View File

@@ -0,0 +1,939 @@
# MESCommonBase 响应输出与 SqlWebCall 默认分支详细文档
文档范围:流程图中下半部分的两块内容。
- `响应输出`
- `DataLink.SqlWebCall 默认分支`
关联入口:`MES_Manage/submit/MESCommonBase.ashx`
生成时间2026-07-02
## 1. 相关程序清单
| 程序文件 | 关键位置 | 作用 |
| --- | --- | --- |
| `MES_Manage/submit/MESCommonBase.ashx` | `ProcessRequest``switch(type)` 和外层 `catch` | 负责把不同 Type 的结果写入 HTTP 响应 |
| `02DataLinkMesWork/DataLinkMesWork.SqlWebCall.cs` | `DataLink.SqlWebCall(int type, JsonData jsonData, jsonobj dataobj)` | 默认分支二次分发入口 |
| `02DataLinkMesWork/DataLinkMesWork.SqlWebCall.cs` | `InitSystemReg(...)` | 初始化数据库连接字符串 |
| `02DataLinkMesWork/DataLinkMesWork.SqlWebCall.cs` | `GetString_JsonData(...)` | 从新协议 JSON 读取 `Type/Name/Param/token` 等字段 |
| `02DataLinkMesWork/DataLinkMesWork.cs` | `ExePROCEDURE_Type*``ExecuteInsertMesWork``ExecuteSelectMesWork` | 具体 SQL、存储过程、登录、分页、返回格式处理 |
| `02DataLinkMesWork/P0.MES.Common/bizDataAccess/SQLCommon.cs` | `ExecuteStoredProcedure``ExecuteDataTable``ExecuteDataset``ExecuteInsertMesWork``ExecuteSelectMesWork` | 底层 ADO.NET 数据库执行封装 |
| `01BasicData/BasicData/BasicData.cs` | `jsonobj` | 旧协议请求对象 |
| `MES_Manage/Web.config` | `appSettings["ConnectionString"]` | 默认数据库连接配置来源 |
## 2. 这部分在总流程中的位置
`MESCommonBase.ashx` 先解析请求体或 `param` 参数,拿到 `type` 后进入 `switch(type)`
流程图下半部分对应的是:
```text
switch(type)
|
|-- 文件下载分支 2001/2002/2003/2004/16 -> 响应输出:二进制下载
|-- 文件上传分支 15 -> 响应输出JSON 文本
|-- IP 查询分支 4000 -> 响应输出:文本 IP
|-- default -> DataLink.SqlWebCall(...) -> 响应输出JSON 文本
```
也就是说,`响应输出` 是所有分支最终面向 HTTP 客户端的出口;`DataLink.SqlWebCall 默认分支` 是普通业务请求的二次路由器。
## 3. 响应输出:程序流程
### 3.1 默认响应头
`ProcessRequest` 进入后先执行:
```csharp
context.Response.ContentType = "application/json";
```
因此除文件下载分支外,默认都会以 `application/json` 作为响应类型。需要注意:`Type=4000` 实际写出的是纯文本 IP但 Content-Type 没有改成 `text/plain`
### 3.2 二进制下载响应
适用 Type
- `2001`
- `2002`
- `2003`
- `2004`
- `16`
典型代码模式:
```csharp
HttpContext.Current.Response.ContentType = "application/octet-stream";
HttpContext.Current.Response.AddHeader(
"Content-Disposition",
"attachment; filename=" + HttpUtility.UrlEncode(fileName, Encoding.UTF8)
);
HttpContext.Current.Response.AddHeader(
"Access-Control-Expose-Headers",
"Content-Disposition"
);
HttpContext.Current.Response.BinaryWrite(bytes);
HttpContext.Current.Response.Flush();
HttpContext.Current.Response.End();
```
字段含义:
| 字段 | 含义 |
| --- | --- |
| `ContentType = application/octet-stream` | 告诉浏览器按二进制文件处理 |
| `Content-Disposition = attachment` | 触发浏览器下载,而不是直接打开 |
| `filename=...` | 下载文件名,使用 UTF-8 URL 编码处理中文文件名 |
| `Access-Control-Expose-Headers` | 允许跨域前端读取 `Content-Disposition` 响应头 |
| `BinaryWrite(bytes)` | 写出文件二进制 |
| `Flush()` | 刷新响应缓冲区 |
| `End()` / `Close()` | 结束响应 |
不同文件分支的差异:
| Type | 生成文件的程序 | 文件名来源 | 结束方式 |
| --- | --- | --- | --- |
| `2001` | `ExcelWebCall.ExcelFile(...)` | `fileName + "." + fileExtension` | `Response.End()` |
| `2002` | `ExcelWebCall.ExcelFilePdf(...)` | `fileName + "." + fileExtension`,通常为 PDF | `Response.End()` |
| `2003` | `ExcelWebCall.ExcelFile(...)` | `fileName + "." + dataimg[0]` | `Response.Close()` |
| `2004` | `DataLink.ExePROCEDURE_Type2004(...)` | 数据库返回文件名和扩展名 | `Response.End()` |
| `16` | `DataLink.ExePROCEDURE_Type16(...)` | 数据库返回文件名和后缀 | `Response.End()` |
### 3.3 Type=16 的空文件处理
`Type=16` 下载前有一层空判断:
```csharp
DataLink.ExePROCEDURE_Type16(jsonData, out bytes, out fileName, out suffix);
if (bytes == null) return;
```
如果下游没有返回文件二进制,则直接 `return`,不会写 JSON 错误,也不会写文件响应。调用方看到的可能是空响应。
### 3.4 Type=15 上传文件后的 JSON 响应
`Type=15` 是上传文件分支,不返回下载流。
流程:
```text
读取 context.Request.Files
|
|-- 有文件:
| 读取第一个文件 InputStream 为 byte[]
| 从 FileName 拆 name/suffix
| 调 DataLink.ExePROCEDURE_Type15(jsonData, name, suffix, bytes)
| Response.Write(responseText)
|
|-- 无文件:
name=""
suffix=""
bytes=new byte[1]
调同一个 ExePROCEDURE_Type15
Response.Write(responseText)
```
`responseText` 来自下游存储过程调用,常见返回:
```json
[{"result":"1"}]
```
或:
```json
[{"result":"0"}]
```
### 3.5 Type=4000 的文本响应
`Type=4000` 返回请求 IP
```csharp
context.Response.Write(userIP);
```
当前 IP 判断逻辑:
```csharp
if (context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != "")
userIP = context.Request.ServerVariables["REMOTE_ADDR"];
else
userIP = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (userIP == null || userIP == "")
userIP = context.Request.UserHostAddress;
```
注意:这里逻辑疑似写反。通常 `HTTP_X_FORWARDED_FOR` 有值时才优先取它;当前代码在有转发头时反而取 `REMOTE_ADDR`
### 3.6 default 分支的 JSON 响应
普通业务请求进入 default
```csharp
responseText = DataLinkMesWork.DataLink.SqlWebCall(type, jsonData, dataobj);
context.Response.Headers.Remove("Server");
context.Response.Write(responseText);
```
特点:
- 响应体完全由 `SqlWebCall` 返回值决定。
- 尝试移除 `Server` 响应头。
- 仍使用入口处默认的 `application/json`
- `SqlWebCall` 对未支持 Type 返回空字符串。
### 3.7 外层异常响应
`ProcessRequest` 外层包了一个总 `try/catch`
```csharp
string responseText = "NULL";
try
{
...
}
catch(Exception err)
{
context.Response.Write(responseText);
}
```
异常后的响应行为:
- 不记录异常。
- 不设置 HTTP 状态码。
- 写出当前 `responseText`
- 如果异常发生在下游调用前,返回 `"NULL"`
- 如果异常发生在 `responseText` 已赋值之后,可能返回旧结果。
## 4. DataLink.SqlWebCall 默认分支:入口流程
入口签名:
```csharp
public static string SqlWebCall(int type, JsonData jsonData, jsonobj dataobj)
```
调用来源:
```csharp
DataLinkMesWork.DataLink.SqlWebCall(type, jsonData, dataobj)
```
调用时机:`MESCommonBase.ashx``switch(type)` 没有匹配到 `2001/2002/2003/2004/15/16/4000` 时进入。
### 4.1 初始化数据库连接
`SqlWebCall` 开始时检查静态字段 `initSystemIsOk`
```csharp
if (!initSystemIsOk)
{
if (!InitSystemReg(out resultReg))
{
return resultReg;
}
}
```
`InitSystemReg` 当前实际逻辑:
```csharp
connectionString = ConfigurationManager.AppSettings["ConnectionString"];
initSystemIsOk = true;
return true;
```
说明:
- 第一次进入 `SqlWebCall` 时,从 `Web.config` 读取 `ConnectionString`
- 成功后设置静态标记,后续调用复用。
- 原先注册号、序列号、加密连接串等校验逻辑已经被注释。
### 4.2 二次分发
初始化后进入 `switch(type)`
```text
SqlWebCall(type,jsonData,dataobj)
|
|-- 8888 / 5001 / 5002加密、注册/改密、登录
|-- 1 / 2 / 5旧协议存储过程
|-- 11 / 111 / 12 / 13 / 21新协议存储过程
|-- 1001 / 1002 / 3 / 4 / 7 / 22 / 3001SQL、建表导入、SQL 命令
```
`SqlWebCall` 自身不直接访问数据库;它只选择具体方法。真正数据库执行在:
- `DataLinkMesWork.cs`
- `SQLCommon.cs`
## 5. SqlWebCall 支持的 Type 明细
### 5.1 登录、注册、加密类
| Type | 方法 | 输入对象 | 主要流程 | 返回 |
| --- | --- | --- | --- | --- |
| `8888` | `GetEncryptStr(jsonData)` | 新协议 `JsonData` | 读取 `Name/name`,用 `AesKeyIvGenerator.Encrypt(name1, SN, SN)` 加密 | 加密字符串,失败默认 `[]` |
| `5001` | `ExePROCEDURE_Type11_AddUser(jsonData)` | 新协议 `JsonData` | 读取 `Param` 数组,找到 `Password` 后 MD5再执行 `Name` 指定存储过程 | 存储过程首表 JSON 或 `[]` |
| `5002` | `ExePROCEDURE_Type11_Login(jsonData)` | 新协议 `JsonData` | 解密前端密码,执行 `Name` 指定存储过程查账号,再比对数据库密码,成功后写入 token | 成功返回用户表 JSON 并追加 `token`,失败返回 `result=0,msg=...` |
登录成功后会调用固定存储过程:
```text
权限管理_Token_增加数据
```
校验 token 时调用:
```text
权限管理_Token_查询数据
```
### 5.2 旧协议存储过程类
旧协议使用 `BasicData.jsonobj dataobj`,字段为:
```csharp
public string Type;
public string ModularID;
public string Name;
public string Param;
public string UserID;
public string Pagination;
public string token;
public bool HasReturn;
```
| Type | 方法 | 执行对象 | 参数格式 | 返回 |
| --- | --- | --- | --- | --- |
| `1` | `ExePROCEDURE_Type1(dataobj)` | `Name` 的第一个 `&` 前作为存储过程名 | `@p=value=type&@p2=value=type` | 首个 DataTable JSON分页时返回带总数结构失败 `result=0` |
| `2` | `ExePROCEDURE_Type2(dataobj)` | `Name` 指定存储过程 | 同 Type 1 | 成功 `result=1`,失败 `result=0` |
| `5` | `ExePROCEDURE_Type5(dataobj)` | `Name` 指定存储过程 | `@p&value&type|@p2&value&type` | 首个 DataTable JSON参数错误返回中文错误字符串 |
旧协议参数解析在 `SQLCommon.GetCmdParam(...)` 中完成:
```text
多个参数:用 & 分隔
单个参数:参数名=值=类型
输出参数:参数名=值=类型=output
```
支持类型:
- `int`
- `string`
- `boolean`
- `datetime`
- 默认按字符串处理
token 行为:
- `Type=1/2/3/4/5` 都是“如果 token 非空则校验”。
- 不传 token 时通常不会拒绝执行。
### 5.3 新协议存储过程类
新协议使用 `JsonData jsonData`,字段从请求 JSON 中读取:
```json
{
"type": "11",
"name": "存储过程名",
"param": "[{\"name\":\"@p\",\"value\":\"v\",\"type\":\"string\"}]",
"token": "..."
}
```
通用字段读取由 `GetString_JsonData(...)` 完成,兼容大小写:
| 大写字段 | 小写字段 | 含义 |
| --- | --- | --- |
| `Type` | `type` | Type |
| `Name` | `name` | 存储过程名或 SQL 文本 |
| `Param` | `param` | 参数数组字符串 |
| `UserID` | `userID` | 用户 |
| `Pagination` | `pagination` | 旧分页参数 |
| `HasReturn` | `hasReturn` | 是否返回 |
| `ModularID` | `modularID` | 模块 |
| `token` | `token` | token |
#### Type=11查询型存储过程
方法:
```csharp
ExePROCEDURE_Type11(jsonData)
```
流程:
```text
读取 Name/Param/token
|
|-- token 非空CheckToken(token),失败返回 [{"result":"3"}]
|
|-- Param 为空:无参数执行存储过程
|-- Param 非空:解析 JSON 数组为 SqlParameter[]
|
|-- SQLCommon.ExecuteStoredProcedure(... out DataSet ...)
|
|-- 有结果表:
|-- 有 pageSize/pageList内存分页返回分页 JSON
|-- 无分页字段:返回首个 DataTable JSON
|
|-- 有 output 参数:包装为 {"result":..., "output":...}
```
#### Type=111服务端分页存储过程
方法:
```csharp
ExePROCEDURE_Type111(jsonData)
```
它解决“通讯服务器分页”的问题。
如果请求中包含:
```json
{
"pageSize": 20,
"pageList": 1
}
```
程序会自动向参数数组追加 4 个参数:
| 参数名 | 值 | 说明 |
| --- | --- | --- |
| `PageCurrent` | `pageList` | 当前页 |
| `PageSize` | `pageSize` | 每页条数 |
| `PageCount` | `1111``type=int``output=1` | 输出参数 |
| `ItemCount` | `1111``type=int``output=1` | 输出参数,总记录数 |
执行后返回:
```json
{
"rows": [],
"total": "总记录数"
}
```
其中 `total` 来自输出参数 `ItemCount`
#### Type=12执行型存储过程
方法:
```csharp
ExePROCEDURE_Type12(jsonData)
```
特点:
- 执行 `Name/name` 指定存储过程。
- 不关注 DataSet。
- 成功返回:
```json
[{"result":"1"}]
```
- 失败返回:
```json
[{"result":"0"}]
```
- 如果有输出参数,返回:
```json
{
"result": [{"result":"1"}],
"output": [{"参数名":"参数值"}]
}
```
#### Type=13返回 DataSet JSON
方法:
```csharp
ExePROCEDURE_Type13(jsonData)
```
特点:
- 执行 `Name/name` 指定存储过程。
- 返回整个 `DataSet` 的 JSON而不是只返回第一张表。
- 有输出参数时包装为:
```json
{
"result": { "DataSet序列化结果": "..." },
"output": [{"参数名":"参数值"}]
}
```
#### Type=21新响应结构的存储过程
方法:
```csharp
ExePROCEDURE_Type21(jsonData)
```
特点:
- 仍然执行 `Name/name` 指定存储过程。
- 返回结构改为:
```json
{
"code": "200",
"message": "",
"data": {}
}
```
或:
```json
{
"code": "500",
"message": "错误信息",
"data": {}
}
```
注意:这里是否成功主要看底层返回的 `result` 字符串是否为空,不完全等同于数据库是否返回数据。
### 5.4 SQL 文本和建表导入类
| Type | 方法 | 执行对象 | 返回 |
| --- | --- | --- | --- |
| `1001` | `ExecuteInsertMesWork(jsonData)` | `Name/name` 作为 SQL 文本,参数来自 `Param` JSON 数组 | 成功 `result=1`;有输出时返回 `output` |
| `1002` | `ExecuteSelectMesWork(jsonData)` | `Name/name` 作为 SQL 查询文本,参数来自 `Param` JSON 数组 | DataTable JSON支持 `pageSize/pageList` 内存分页 |
| `3` | `ExePROCEDURE_Type3(dataobj)` | `dataobj.Name` 作为 SQL 查询文本 | DataTable JSON |
| `4` | `ExePROCEDURE_Type4(dataobj)` | `dataobj.Name` 作为非查询 SQL 文本 | 成功 `result=1` |
| `7` | `ExePROCEDURE_Type7(jsonData)` | `Name/name` 作为表名,`Param` 作为行数据 | 删除并重建表,批量插入,成功 `result=1` |
| `22` | `ExePROCEDURE_Type22(dataobj)` | `dataobj.Name` 作为 SQL 文本 | `{ code, message, data }` |
| `3001` | `DbCallType1003_SqlCmd.SqlExec(jsonData)` | SQL 命令执行入口 | 由该方法决定 |
这类 Type 的共性:
- 执行对象来自客户端 `Name/name`
- 参数虽然使用 `SqlParameter` 绑定,但 SQL 文本或表名本身不是固定白名单。
- 需要调用方和网络边界可信,否则风险很高。
## 6. SqlWebCall 参数处理流程
### 6.1 新协议 Param 数组字符串
常见格式:
```json
{
"type": "11",
"name": "存储过程名",
"param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"},{\"name\":\"@数量\",\"value\":\"10\",\"type\":\"int\"}]"
}
```
处理逻辑:
```text
param 字符串
|
|-- JsonMapper.ToObject(param)
|
|-- 遍历数组
|-- nameSqlParameter 名
|-- value参数值如果是数组转为逗号字符串并做 Unicode 解码
|-- type存在时做类型转换
|-- output == "1":改为输出参数
|
|-- SQLCommon.ExecuteStoredProcedure / ExecuteSelectMesWork / ExecuteInsertMesWork
```
支持类型主要为:
- `int`
- `string`
- `boolean`
- `bool`
- `datetime`
- 默认字符串
### 6.2 旧协议 Param 拼接字符串
Type 1/2 的旧协议格式:
```text
@p1=value1=string&@p2=10=int&@out=0=int=output
```
`SQLCommon.GetCmdParam(...)` 处理逻辑:
```text
Param
|
|-- 按 & 分割参数
|-- 过滤包含 == / null / undefined 的项
|-- 每项按 = 分割
|-- 长度 2直接字符串参数
|-- 长度 3按类型转换
|-- 长度 4 且第 4 项为 output设为输出参数
```
Type 5 的旧协议格式不同:
```text
@p1&value1&String|@p2&10&Int
```
## 7. 底层数据库执行链路
### 7.1 存储过程查询
典型调用链:
```text
MESCommonBase.ashx default
-> DataLink.SqlWebCall(...)
-> DataLink.ExePROCEDURE_Type11/13/21(...)
-> SQLCommon.ExecuteStoredProcedure(...)
-> SqlConnection / SqlDataAdapter
-> DataSet / DataTable
-> JsonHelper / JsonConvert 序列化
-> Response.Write(responseText)
```
`SQLCommon.ExecuteStoredProcedure(... out DataSet ...)` 的关键行为:
- 创建 `SqlConnection`
- 创建 `SqlCommand(procedureName, conn)`
- 设置 `CommandType = StoredProcedure`
- 设置 `CommandTimeout = 0`,即不限制命令超时。
- 添加 `SqlParameter[]`
- 使用 `SqlDataAdapter.Fill(ds)` 填充结果集。
- 异常时写 `ApplicationLog`,并通过 `errorMessage` 返回异常字符串。
### 7.2 SQL 查询
典型调用链:
```text
Type 1002 / 3 / 22
-> SQLCommon.ExecuteSelectMesWork 或 ExecuteDataTable / ExecuteDataset
-> SqlDataAdapter(sql, conn)
-> Fill(DataTable/DataSet)
-> JSON
```
### 7.3 SQL 增删改
典型调用链:
```text
Type 1001 / 4
-> SQLCommon.ExecuteInsertMesWork 或 ExecuteNonQuery
-> SqlCommand.CommandText = SQL 文本
-> ExecuteNonQuery / ExecuteScalar
-> result=1 或 result=0
```
## 8. 返回格式汇总
### 8.1 成功返回首表 JSON
常见于:
- `Type=1`
- `Type=3`
- `Type=5`
- `Type=11`
- `Type=1002`
示例:
```json
[
{
"字段1": "值1",
"字段2": "值2"
}
]
```
### 8.2 成功/失败标志
常见于:
- `Type=2`
- `Type=4`
- `Type=12`
- `Type=1001`
成功:
```json
[{"result":"1"}]
```
失败:
```json
[{"result":"0"}]
```
### 8.3 token 失败
部分方法在 token 非空且校验失败时返回:
```json
[{"result":"3"}]
```
### 8.4 输出参数包装
普通输出参数:
```json
{
"result": [{"result":"1"}],
"output": [{"@out":"123"}]
}
```
分页输出参数:
```json
{
"rows": [],
"total": "123"
}
```
### 8.5 新结构响应
常见于:
- `Type=21`
- `Type=22`
```json
{
"code": "200",
"message": "",
"data": {}
}
```
失败:
```json
{
"code": "500",
"message": "错误信息",
"data": {}
}
```
### 8.6 空或未支持 Type
`SqlWebCall` 对未匹配 Type 没有 default 处理,`result` 初始为空字符串。
因此 `MESCommonBase.ashx` default 分支可能返回空响应体:
```text
""
```
如果进入外层异常,则可能返回:
```text
NULL
```
## 9. 鉴权与 token 逻辑
`CheckToken(token)` 逻辑:
```text
token == "" -> true
token 非空:
调 权限管理_Token_查询数据
|
|-- 存储过程报错 -> false
|-- 返回空表 -> false
|-- 返回有数据 -> true
```
关键影响:
- 多数业务方法只有在 `token` 非空时才调用 `CheckToken`
- 如果请求不传 token常见路径不会拒绝。
- `SqlWebCall` 入口层没有统一鉴权。
- 因此权限控制依赖调用方是否传 token以及下游存储过程是否自行校验。
## 10. 风险点与维护注意事项
### 10.1 响应层风险
1. `Response.End()` 位于大 try 内,可能触发 `ThreadAbortException`,随后外层 catch 写出 `responseText`
2. 下载失败时没有统一 JSON 错误,`Type=16``bytes == null` 会直接空返回。
3. `Type=4000` 返回纯文本,但 Content-Type 仍可能是 `application/json`
4. 外层异常不记录日志,定位问题困难。
5. 多种响应格式混用,前端必须按 Type 分别处理。
### 10.2 SqlWebCall 风险
1. 客户端可通过 `Name/name` 指定存储过程名、SQL 文本或表名。
2. `Type=3/4/22/1001/1002/3001` 存在直接 SQL 执行能力。
3. `Type=7` 可根据请求表名执行 `DROP TABLE``CREATE TABLE`
4. token 校验不是统一强制的。
5. `CommandTimeout = 0` 可能导致长时间阻塞。
6. 失败返回经常只有 `result=0`,缺少错误原因。
### 10.3 参数层风险
1. 新协议 `Param` 是 JSON 字符串,不是 JSON 对象;调用方需要二次转义。
2. 输出参数类型转换依赖 `type``output` 字段,格式错误会进入 catch 并返回 `result=0`
3. 数组参数被拼成逗号字符串,数组元素里如果本身包含逗号,语义会丢失。
4. 旧协议分隔符为 `&``=``|`,参数值包含这些字符时容易解析错误。
## 11. 建议改进方案
### 11.1 低成本修复
-`MESCommonBase.ashx` default 分支前统一校验 token。
- 未支持 Type 返回明确错误:
```json
{"code":"400","message":"Unsupported Type","data":null}
```
- 下载分支改为 `HttpContext.Current.ApplicationInstance.CompleteRequest()`,避免 `Response.End()` 的线程中止异常。
- `Type=4000` 设置 `ContentType = "text/plain"`,并修正代理 IP 判断逻辑。
- 外层 catch 记录异常Type、Name、IP、异常堆栈。
### 11.2 中期改造
- 建立 `Type + Name` 白名单,禁止任意存储过程名和 SQL 文本。
-`Param` 从“JSON 字符串”改为真正的 JSON 数组字段。
- 统一 JSON 返回结构:
```json
{
"code": "200",
"message": "",
"data": {}
}
```
- 将 SQL 执行类 Type 与普通业务接口分离。
### 11.3 长期改造
- 按业务模块拆分接口,减少万能网关。
- 数据库账号按能力拆分权限。
- 移除明文连接字符串,使用部署环境密钥。
- 对文件下载、上传、SQL 执行加入审计日志和限流。
## 12. 典型流程示例
### 12.1 Type=11 查询型存储过程
请求:
```json
{
"type": "11",
"name": "MES_工位_查询",
"param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]",
"token": "token值"
}
```
流程:
```text
MESCommonBase.ashx default
-> SqlWebCall(11,jsonData,dataobj)
-> InitSystemReg 读取连接字符串
-> ExePROCEDURE_Type11
-> GetString_JsonData 读取 name/param/token
-> token 非空则 CheckToken
-> Param 转 SqlParameter[]
-> SQLCommon.ExecuteStoredProcedure
-> JsonHelper.DataTableToJson
-> Response.Write(JSON)
```
### 12.2 Type=111 分页型存储过程
请求:
```json
{
"type": "111",
"name": "MES_工位_分页查询",
"param": "[{\"name\":\"@关键字\",\"value\":\"OP\",\"type\":\"string\"}]",
"pageSize": 20,
"pageList": 1
}
```
内部自动追加分页参数:
```text
PageCurrent = 1
PageSize = 20
PageCount = 输出参数
ItemCount = 输出参数
```
响应:
```json
{
"rows": [],
"total": "100"
}
```
### 12.3 Type=1002 SQL 查询
请求:
```json
{
"type": "1002",
"name": "select * from Test where f1=@f1",
"param": "[{\"name\":\"@f1\",\"value\":\"T002\",\"type\":\"string\"}]"
}
```
流程:
```text
SqlWebCall(1002)
-> ExecuteSelectMesWork(jsonData)
-> SQLCommon.ExecuteSelectMesWork(sql, params, connectionString, out dt)
-> DataTableToJson
```
说明:参数值通过 `SqlParameter` 绑定,但 SQL 文本本身来自请求。
## 13. 结论
流程图中的 `响应输出` 是整个 `MESCommonBase.ashx` 的 HTTP 出口负责二进制下载、JSON 文本、IP 文本和异常兜底输出。`DataLink.SqlWebCall 默认分支` 是普通业务请求的二次分发中心,它按 Type 调用不同数据库执行方法覆盖登录、存储过程、SQL 查询、SQL 增删改、分页和新结构返回。
这部分程序的核心问题不是流程复杂,而是执行权过于通用:客户端可通过 `type + name + param` 控制大量数据库行为。维护时应优先关注统一鉴权、Type/Name 白名单、异常日志和响应格式统一。

View File

@@ -0,0 +1,58 @@
%% MESCommonBase.ashx 主流程图
flowchart TD
START([HTTP 请求<br/>MES_Manage/submit/MESCommonBase.ashx])
READ[ProcessRequest<br/>设置 ContentType=application/json<br/>读取 Request.InputStream]
PARSE1[JsonMapper.ToObject(stream)<br/>尝试读取 Type / type]
PARSE2[JavaScriptSerializer.Deserialize&lt;jsonobj&gt;(stream)<br/>成功后用 dataobj.Type 覆盖 type]
PARAM{dataobj == null?}
PARAMYES[读取 Request["param"]<br/>再次按 JSON 解析 Type / type]
SWITCH{switch(type)}
START --> READ --> PARSE1 --> PARSE2 --> PARAM
PARAM -->|是| PARAMYES --> SWITCH
PARAM -->|否| SWITCH
SWITCH -->|2001| T2001[Excel 导出<br/>ExcelWebCall.ExcelFile(jsonData)<br/>生成 bytes/fileName/extension]
SWITCH -->|2002| T2002[PDF 导出<br/>ExcelWebCall.ExcelFilePdf(jsonData)<br/>Excel 转 PDF]
SWITCH -->|2003| T2003[合成 Excel 图片/指定扩展名导出<br/>读取 Form 第一项 dataimg[0]<br/>ExcelWebCall.ExcelFile(jsonData)]
SWITCH -->|2004| T2004[数据库文件下载<br/>DataLink.ExePROCEDURE_Type2004(jsonData)<br/>返回 bytes/fileName/fileExtension]
SWITCH -->|15| T15[文件上传<br/>读取 Request.Files[0]<br/>拆分 name/suffix/bytes<br/>DataLink.ExePROCEDURE_Type15]
SWITCH -->|16| T16[数据库文件下载<br/>DataLink.ExePROCEDURE_Type16(jsonData)<br/>返回 fileName/suffix/bytes]
SWITCH -->|4000| T4000[IP 查询<br/>读取 ServerVariables / UserHostAddress]
SWITCH -->|default| DEFAULT[通用业务调用<br/>DataLink.SqlWebCall(type,jsonData,dataobj)]
T2001 --> DOWNLOAD[二进制下载响应<br/>Content-Type=application/octet-stream<br/>Content-Disposition=attachment<br/>BinaryWrite + Flush/End]
T2002 --> DOWNLOAD
T2003 --> DOWNLOAD
T2004 --> DOWNLOAD
T16 --> BYTESNULL{bytes == null?}
BYTESNULL -->|是| RETURNEMPTY[直接 return<br/>不写响应体]
BYTESNULL -->|否| DOWNLOAD
T15 --> JSONRESP[JSON 文本响应<br/>Response.Write(responseText)]
T4000 --> IPRESP[文本响应<br/>Response.Write(userIP)]
DEFAULT --> SQLWEB
subgraph SQLWEB[DataLink.SqlWebCall 默认分支]
S1[初始化连接<br/>InitSystemReg 读取 Web.config: ConnectionString]
S2{按 type 再分发}
S3[登录/加密<br/>8888: GetEncryptStr<br/>5001: AddUser/改密<br/>5002: Login/生成 token]
S4[旧协议存储过程<br/>1/2/5<br/>Param 为拼接字符串]
S5[新协议存储过程<br/>11/111/12/13/21<br/>Param 为 JSON 数组字符串]
S6[直接 SQL / 导入<br/>1001/1002/3/4/7/22/3001]
S7[返回 JSON<br/>数组 JSON 或 {code,message,data}]
S1 --> S2
S2 --> S3 --> S7
S2 --> S4 --> S7
S2 --> S5 --> S7
S2 --> S6 --> S7
end
SQLWEB --> JSONRESP
DOWNLOAD --> END([请求结束])
JSONRESP --> END
IPRESP --> END
RETURNEMPTY --> END
READ -. 外层 try/catch异常被吞掉写出当前 responseText初始为 NULL .-> CATCH[异常处理<br/>catch(Exception err)<br/>Response.Write(responseText)]
CATCH --> END

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

View File

@@ -0,0 +1,461 @@
# MESCommonBase.ashx 程序梳理
梳理对象:`MES_Manage/submit/MESCommonBase.ashx`
梳理时间2026-07-02
## 1. 程序定位
`MESCommonBase.ashx` 是一个 ASP.NET WebHandler类名为 `MESCommonBase`,实现 `IHttpHandler`。它不是单一业务接口,而是 MES Web 端的通用提交网关:
- 接收 HTTP 请求。
- 从请求体 JSON 或 `param` 参数中解析 `Type/type`
-`Type` 分发到文件导出、文件上传、文件下载、IP 查询或通用数据库调用。
- 普通业务请求最终委托给 `DataLinkMesWork.DataLink.SqlWebCall(...)`
入口方法:
- `ProcessRequest(HttpContext context)`
- `IsReusable = false`
主要依赖:
- `BasicData.jsonobj`:旧协议请求对象,字段包括 `Type/Name/Param/UserID/Pagination/token/HasReturn/ModularID`
- `DataLinkMesWork.JsonData``JsonMapper`:本项目内置 JSON 类型与解析器,不是 Newtonsoft 的 `JObject`
- `DataLinkMesWork.DataLink`SQL、存储过程、文件上传下载的实际执行层。
- `MESDownloadExcel.ExcelWebCall`Excel/PDF 导出。
- `Web.config``appSettings["ConnectionString"]`:数据库连接字符串来源。
## 2. 总体执行流程
```text
HTTP 请求进入 MESCommonBase.ashx
|
|-- 默认设置 Response.ContentType = application/json
|-- 读取 Request.InputStream 为 stream
|
|-- 第一次解析JsonMapper.ToObject(stream)
| 成功则尝试读取 Type 或 type
|
|-- 第二次解析JavaScriptSerializer.Deserialize<jsonobj>(stream)
| 成功则用 dataobj.Type 覆盖 type
|
|-- 如果 dataobj == null
| 尝试读取 HttpContext.Current.Request["param"]
| 再用 JsonMapper 解析并读取 Type/type
|
|-- switch(type)
|-- 2001/2002/2003/2004生成并下载文件
|-- 15上传文件到数据库或存储过程
|-- 16从数据库或存储过程取文件并下载
|-- 4000返回请求 IP
|-- defaultDataLink.SqlWebCall(type, jsonData, dataobj)
```
## 3. 请求解析规则
### 3.1 支持的入参来源
程序支持三类请求载荷:
1. JSON 请求体:
```json
{
"type": "11",
"name": "存储过程名",
"param": "[{\"name\":\"@id\",\"value\":\"1\",\"type\":\"int\"}]"
}
```
2. 旧对象协议请求体:
```json
{
"Type": "1",
"Name": "存储过程名",
"Param": "@id=1=int",
"Pagination": "1&20",
"token": "..."
}
```
3. 表单或查询参数 `param`
```text
param={"type":"15","name":"上传文件存储过程","param":"[...]"}
```
第 3 种主要用于 multipart 上传场景,因为 multipart 请求体无法直接按 JSON 解析。
### 3.2 Type 读取优先级
实际代码中的优先级为:
1. `JsonMapper.ToObject(stream)` 后读取 `Type/type`
2. `JavaScriptSerializer.Deserialize<jsonobj>(stream)` 成功后,用 `dataobj.Type` 覆盖前面的 `type`
3. 如果 `dataobj == null`,读取 `Request["param"]`,再解析 `Type/type`
注意:所有解析异常都被吞掉,失败时 `type` 保持默认值 `0`,最终进入默认分支。
## 4. MESCommonBase.ashx 的 Type 分支
| Type | 分支用途 | 下游调用 | 响应 |
| --- | --- | --- | --- |
| `2001` | 生成 Excel 文件并下载 | `ExcelWebCall.ExcelFile(jsonData, out bytes, out fileName, ref fileExtension)` | `application/octet-stream` 二进制下载 |
| `2002` | 生成 PDF 文件并下载 | `ExcelWebCall.ExcelFilePdf(jsonData, out bytes, out fileName, out fileExtension)` | `application/octet-stream` 二进制下载 |
| `2003` | 合成 Excel 图片或按表单指定扩展名导出 | `ExcelWebCall.ExcelFile(jsonData, out bytes, out fileName, ref dataimg[0])` | `application/octet-stream` 二进制下载 |
| `2004` | 从存储过程结果中取文件并下载 | `DataLink.ExePROCEDURE_Type2004(jsonData, out bytes, out fileName, out fileExtension)` | `application/octet-stream` 二进制下载 |
| `15` | 上传文件 | `DataLink.ExePROCEDURE_Type15(jsonData, name, suffix, bytes)` | JSON 文本 |
| `16` | 下载数据库文件 | `DataLink.ExePROCEDURE_Type16(jsonData, out bytes, out fileName, out suffix)` | `application/octet-stream` 二进制下载 |
| `4000` | 返回客户端 IP | 直接读取 `ServerVariables` / `UserHostAddress` | 文本 IP |
| 其他 | 通用数据库调用 | `DataLink.SqlWebCall(type, jsonData, dataobj)` | JSON 文本 |
## 5. 默认分支 DataLink.SqlWebCall
默认分支是主要业务入口。`MESCommonBase.ashx` 只负责传入 `type/jsonData/dataobj`,实际能力由 `DataLinkMesWork.DataLink.SqlWebCall` 决定。
当前 `SqlWebCall(int type, JsonData jsonData, jsonobj dataobj)` 支持的主要 Type
| Type | 下游方法 | 作用概括 |
| --- | --- | --- |
| `8888` | `GetEncryptStr(jsonData)` | 返回加密后的字符串,使用请求中的 `Name/name` |
| `5001` | `ExePROCEDURE_Type11_AddUser(jsonData)` | 注册或修改密码相关逻辑 |
| `5002` | `ExePROCEDURE_Type11_Login(jsonData)` | 登录,校验加密密码,成功后生成 token |
| `1001` | `ExecuteInsertMesWork(jsonData)` | 执行客户端传入 SQL偏插入/更新场景 |
| `1002` | `ExecuteSelectMesWork(jsonData)` | 执行客户端传入 SQL返回 DataTable JSON |
| `1` | `ExePROCEDURE_Type1(dataobj)` | 旧协议执行存储过程并返回首表 JSON支持分页和输出参数 |
| `2` | `ExePROCEDURE_Type2(dataobj)` | 旧协议执行存储过程,成功返回 `result=1` |
| `3` | `ExePROCEDURE_Type3(dataobj)` | 直接执行 `Name` 中的 SQL 查询并返回 JSON |
| `4` | `ExePROCEDURE_Type4(dataobj)` | 直接执行 `Name` 中的非查询 SQL |
| `5` | `ExePROCEDURE_Type5(dataobj)` | 旧协议执行存储过程并返回首表 JSON参数格式略不同 |
| `7` | `ExePROCEDURE_Type7(jsonData)` | 按 Vue/Excel 数据创建或重建数据库表并批量插入 |
| `11` | `ExePROCEDURE_Type11(jsonData)` | 新协议执行存储过程并返回首表 JSON |
| `111` | `ExePROCEDURE_Type111(jsonData)` | 新协议分页存储过程,自动追加分页入参和输出参数 |
| `12` | `ExePROCEDURE_Type12(jsonData)` | 新协议执行存储过程,成功返回 `result=1` |
| `13` | `ExePROCEDURE_Type13(jsonData)` | 新协议执行存储过程,返回整个 DataSet JSON |
| `21` | `ExePROCEDURE_Type21(jsonData)` | 新结构返回:`{ code, message, data }` |
| `22` | `ExePROCEDURE_Type22(dataobj)` | 执行 `Name` 中 SQL返回 `{ code, message, data }` |
| `3001` | `DbCallType1003_SqlCmd.SqlExec(jsonData)` | SQL 命令执行入口 |
`SqlWebCall` 初始化时调用 `InitSystemReg()`,该方法直接从 `appSettings["ConnectionString"]` 读取连接字符串,并把 `initSystemIsOk` 置为 `true`。原来的注册或授权校验逻辑已被注释。
## 6. 通用请求字段
新旧协议里字段大小写混用,`DataLink.GetString_JsonData(...)` 同时兼容大写和小写:
| 字段 | 含义 |
| --- | --- |
| `Type` / `type` | 第一层分发类型 |
| `Name` / `name` | 存储过程名、SQL 文本、表名或导出配置入口,具体含义由 Type 决定 |
| `Param` / `param` | 参数。新协议通常是 JSON 数组字符串,旧协议是拼接字符串 |
| `UserID` / `userID` | 用户标识,部分日志或存储过程使用 |
| `Pagination` / `pagination` | 旧分页参数,形如 `页码&每页行数`,但不同方法里的顺序存在差异 |
| `pageSize` / `pageList` | 新分页参数 |
| `HasReturn` / `hasReturn` | 是否有返回值,当前入口未直接使用 |
| `ModularID` / `modularID` | 子组件或模块标识,主要用于日志 |
| `token` | token。只有下游部分方法在 token 非空时校验 |
## 7. Param 参数格式
### 7.1 新协议 JSON 数组字符串
`Type=11/12/13/15/16/21/111/2004` 等大量方法使用这种格式:
```json
{
"type": "11",
"name": "存储过程名",
"param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"},{\"name\":\"@数量\",\"value\":\"10\",\"type\":\"int\"}]"
}
```
参数对象常见字段:
| 字段 | 含义 |
| --- | --- |
| `name` | SQL 参数名 |
| `value` | 参数值;数组会被拼成逗号字符串 |
| `type` | 可选,支持 `int/string/boolean/bool/datetime` |
| `output` | 值为 `"1"` 时作为输出参数 |
### 7.2 旧协议字符串参数
`Type=1/2` 通过 `SQLCommon.GetCmdParam(jsonobj, ...)` 解析,实际分隔符是:
```text
参数名=参数值=类型
参数名=参数值=类型=output
多个参数用 & 分隔
```
示例:
```text
@工位号=OP10=string&@数量=10=int&@ItemCount=0=int=output
```
`Type=5` 解析方式不同:多个参数用 `|` 分隔,单个参数内部用 `&` 分隔:
```text
参数名&参数值&类型|参数名&参数值&类型
```
## 8. 文件上传下载约定
### 8.1 Type=15 上传文件
入口行为:
- 读取 `context.Request.Files` 的第一个文件。
- 整个文件一次性读入 `byte[]`
- 从文件名最后一个 `.` 拆出 `name``suffix`
- 调用 `DataLink.ExePROCEDURE_Type15(jsonData, name, suffix, bytes)`
下游行为:
-`jsonData.Name/name` 取存储过程名。
-`jsonData.Param/param` 取参数数组。
- 在执行前强制把参数数组的最后 3 个参数值改为:
- 倒数第 3 个:文件名,不含后缀。
- 倒数第 2 个:文件后缀。
- 倒数第 1 个:文件二进制。
因此上传文件对应的存储过程参数定义必须预留最后三个参数用于文件名、后缀和二进制内容。
如果没有上传文件,仍会调用同一个存储过程,只是:
- `name = ""`
- `suffix = ""`
- `bytes = new byte[1]`
### 8.2 Type=16 下载文件
入口行为:
- 调用 `DataLink.ExePROCEDURE_Type16(...)`
- 如果 `bytes == null`,直接返回,不写响应。
- 否则按 `fileName + "." + suffix` 下载。
下游约定:
存储过程返回的第一个 DataTable 第一行必须至少有 3 列:
1. 文件名。
2. 后缀。
3. `byte[]` 文件内容。
### 8.3 Type=2004 下载文件
下游调用 `ExePROCEDURE_Type2004`,该方法执行 `Name/name` 指定的存储过程。返回数据约定为第一个 DataTable 第一行:
1. `byte[]` 文件内容。
2. 文件名。
3. 扩展名。
入口再拼成 `fileName.extension` 下载。
### 8.4 Type=2001/2002/2003 导出 Excel/PDF
这些分支调用 `MESDownloadExcel.ExcelWebCall`
- `2001`:生成 Excel。
- `2002`:先生成 Excel再转换为 PDF。
- `2003`:从表单第一项取扩展名或额外数据,再生成文件。
`ExcelWebCall` 内部通过 `SqlServerCmd.SqlCmd.GetDataSetByJson(...)` 获取数据集,再按 `tabletype/tablecode` 选择 `WriteExcelNPOI` 的不同模板或导出方法。
## 9. 响应行为
普通 JSON 分支:
- 默认 `ContentType = application/json`
- 默认分支会尝试移除 `Server` 响应头。
- 成功或失败 JSON 格式不统一,常见失败为:
```json
[{"result":"0"}]
```
登录失败会带 `msg`Type 21/22 使用:
```json
{"code":"500","message":"...","data":{}}
```
下载分支:
- 设置 `ContentType = application/octet-stream`
- 添加 `Content-Disposition: attachment; filename=...`
- 添加 `Access-Control-Expose-Headers: Content-Disposition`,便于前端跨域读取文件名。
- 使用 `BinaryWrite(bytes)` 写出文件。
外层异常处理:
- `responseText` 初始值是 `"NULL"`
- 任意未处理异常都会被 catch然后写出当前 `responseText`
- 异常对象 `err` 未记录,排查线上问题较困难。
## 10. 数据库与配置
`MES_Manage/Web.config` 中配置:
- 目标框架:`.NET Framework 4.8`
- CORS允许所有来源、常见方法和 `Content-Type`
- 连接字符串:`appSettings["ConnectionString"]`
连接字符串在配置文件中包含明文数据库账号密码。梳理文档不展开具体值,但这是部署与安全审计时需要重点处理的问题。
## 11. 关键风险点
1. 客户端可直接决定存储过程名或 SQL 文本。
`Type=1/2/5/11/12/13/15/16/21/2004` 等使用请求中的 `Name/name` 作为存储过程名;`Type=3/4/22/1001/1002/3001` 等存在直接执行客户端 SQL 文本或 SQL 命令的能力。如果接口暴露到非可信网络,风险很高。
2. token 校验是可选的。
多个下游方法只在 `token` 非空时调用 `CheckToken(token)`;如果不传 token通常不会拒绝请求。`Type=15/16/2004` 等文件分支也没有在入口层做统一鉴权。
3. 外层吞异常,返回值会误导调用方。
解析、分发和下载过程中大量 `catch { }``catch(Exception err)` 不记录日志。下载分支一旦异常,可能返回 `"NULL"` 或半截二进制响应,前端不易判断真实原因。
4. 文件上传一次性读入内存。
`Type=15` 使用 `new byte[files[0].InputStream.Length]` 一次性读取没有文件大小、扩展名、MIME、病毒扫描或上传字段校验。
5. 上传文件名拆分不稳健。
文件名没有 `.` 时,`LastIndexOf(".")``-1``Substring` 会抛异常,最终只返回 `"NULL"`
6. IP 获取逻辑疑似写反。
代码是:
```csharp
if (context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != "")
userIP = context.Request.ServerVariables["REMOTE_ADDR"];
else
userIP = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
```
通常应该在 `HTTP_X_FORWARDED_FOR` 有值时优先取它;当前逻辑相反。
7. `Response.End()` 位于外层 try 内。
`Response.End()` 在 ASP.NET 中可能触发 `ThreadAbortException`。当前外层 catch 会尝试继续写 `responseText`,存在响应污染或异常行为风险。
8. 业务返回格式不统一。
同一个入口可能返回二进制、数组 JSON、对象 JSON、纯文本 IP、`NULL`、异常消息字符串。前端调用需要按 Type 单独处理。
9. 明文数据库连接配置。
`Web.config` 中存在明文连接字符串和高权限数据库账号,建议至少迁移到安全配置或部署环境变量,并降低数据库账号权限。
10. Type=7 可按请求内容 DROP/CREATE 表。
`ExePROCEDURE_Type7` 使用请求中的 `Name/name` 作为表名,拼接 `DROP TABLE``CREATE TABLE`,虽然值插入部分对单引号做了替换,但表名和列名仍来自请求,风险较高。
## 12. 建议改造方向
短期建议:
-`MESCommonBase.ashx` 入口层统一鉴权,不允许无 token 进入数据库执行类 Type。
- 建立 `Type + Name` 白名单,禁止客户端传任意 SQL 或任意存储过程名。
- 给上传文件增加大小、扩展名、字段数量和文件名格式校验。
- 为所有 catch 写日志,至少记录 Type、Name、请求来源 IP、异常消息和堆栈。
- 修正 `Type=4000` 的 IP 获取逻辑。
- 下载分支用 `CompleteRequest()` 替代 `Response.End()`
中期建议:
- 统一请求 DTO避免同时使用 `JsonMapper``JavaScriptSerializer` 和大小写混合字段。
- 统一响应格式。二进制下载保留文件响应JSON 分支统一 `{ code, message, data }`
- 将旧协议 `Param` 拼接字符串逐步迁移到 JSON 数组参数。
- 对 SQL 执行能力做分层:普通业务接口不应暴露直接 SQL 文本执行。
长期建议:
- 把通用网关拆成明确的业务接口,按模块授权、审计和限流。
- 数据库账号按读、写、文件、管理等能力拆分最小权限。
- 移除配置中的明文密码,使用部署环境的密钥管理能力。
## 13. 快速调用示例
### 13.1 新协议执行存储过程并返回表 JSON
```json
{
"type": "11",
"name": "存储过程名",
"param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]",
"token": "token值"
}
```
### 13.2 新协议分页存储过程
```json
{
"type": "111",
"name": "分页存储过程名",
"param": "[{\"name\":\"@关键字\",\"value\":\"abc\",\"type\":\"string\"}]",
"pageSize": 20,
"pageList": 1,
"token": "token值"
}
```
下游会自动追加:
- `PageCurrent`
- `PageSize`
- `PageCount` 输出参数
- `ItemCount` 输出参数
返回结构类似:
```json
{
"rows": [],
"total": "0"
}
```
### 13.3 上传文件
请求方式:`multipart/form-data`
表单字段:
- `param`JSON 字符串,至少包含 `type=15``name``param`
- 文件字段:第一个上传文件会被读取。
示例 `param`
```json
{
"type": "15",
"name": "上传文件存储过程名",
"param": "[{\"name\":\"@业务ID\",\"value\":\"123\",\"type\":\"int\"},{\"name\":\"@文件名\",\"value\":\"\",\"type\":\"string\"},{\"name\":\"@后缀\",\"value\":\"\",\"type\":\"string\"},{\"name\":\"@内容\",\"value\":\"\",\"type\":\"binary\"}]"
}
```
注意:最后三个参数会被代码覆盖为文件名、后缀和二进制内容。
### 13.4 下载数据库文件
```json
{
"type": "16",
"name": "下载文件存储过程名",
"param": "[{\"name\":\"@文件ID\",\"value\":\"123\",\"type\":\"int\"}]"
}
```
存储过程需返回:文件名、后缀、二进制内容。
## 14. 结论
`MESCommonBase.ashx` 是一个高度通用的 MES 通讯入口,核心价值是用一个地址承载 SQL 查询、存储过程调用、Excel/PDF 导出、文件上传和文件下载。但它当前把大量执行权交给客户端请求参数,且鉴权、白名单、异常日志和文件安全控制都比较弱。若该接口面向非可信调用方,应优先补齐入口鉴权和可执行对象白名单,再逐步拆分成明确的业务接口。

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,596 @@
# -*- 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", "旧协议 DTOType/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,
[
"存储过程使用 ExecuteStoredProcedureCommandType=StoredProcedureCommandTimeout=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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

View File

@@ -0,0 +1,261 @@
# -*- 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<jsonobj>(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 PNGworking/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}")

View File

@@ -0,0 +1,322 @@
# -*- 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-DispositionBinaryWrite(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 并加密\n5001Password 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\n7DROP/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 JSONType 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"
"未支持 TypeSqlWebCall 可能返回空字符串。",
3140,
2845,
2290,
font=d.font_note,
)
d.note_box(
560,
3700,
4680,
210,
"次流程风险token 多数是非空才校验;客户端可控制 Name/nameSQL 文本、存储过程名、表名缺少统一白名单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()