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

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*
!/.gitignore
!/README.md
!/MesUniversalApi/
!/MesUniversalApi/**
!/working/
!/working/**
!/working1/
!/working1/**

11
MesUniversalApi/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
bin/
obj/
.vs/
TestResults/
artifacts/
publish/
*.user
*.suo
*.log
.env
.env.*

53
MesUniversalApi/README.md Normal file
View File

@@ -0,0 +1,53 @@
# MesUniversalApi 移植目录
本目录用于承载新的通用 WebAPI 移植项目代码、测试、部署文件、工具脚本和验收附件。
## 目标基线
- 目标 SDK`.NET 10`
- 目标框架:`net10.0`
- 目标形态:`ASP.NET Core Web API`
- 目录角色:新项目代码根目录,不与旧 `MES_Manage/``02DataLinkMesWork/` 混做
## 当前状态
截至 2026-07-02
- 仅创建目录骨架和说明文件。
- 尚未生成 `.sln``.csproj`
- 当前开发机仅检测到 `.NET SDK 9.0.311`,尚未满足 `net10.0` 项目生成条件。
## 目录说明
```text
MesUniversalApi/
src/ 新项目源码
tests/ 单元测试和集成测试
deploy/ Docker、systemd、Nginx 等部署资产
docs/ 项目内技术文档和补充说明
tools/ 辅助脚本
evidence/ 验收附件、截图、报告、回归样本
```
## 开工前检查
1. 安装 `.NET 10 SDK`
2. 运行 `dotnet --list-sdks`,确认输出包含 `10.0.x`
3. 在本目录创建 `global.json`,锁定实际安装的 `10.0.x`
4.`working1/04-任务矩阵.md` 先执行 `T001`,再执行 `T002`
## 安装 SDK 后的首批命令
```bash
cd MesUniversalApi
dotnet new globaljson --sdk-version 10.0.xxx
dotnet new sln -n MesUniversalApi
dotnet new webapi -f net10.0 -n MesUniversalApi.Api -o src/MesUniversalApi.Api
dotnet new classlib -f net10.0 -n MesUniversalApi.Application -o src/MesUniversalApi.Application
dotnet new classlib -f net10.0 -n MesUniversalApi.Contracts -o src/MesUniversalApi.Contracts
dotnet new classlib -f net10.0 -n MesUniversalApi.Domain -o src/MesUniversalApi.Domain
dotnet new classlib -f net10.0 -n MesUniversalApi.Infrastructure -o src/MesUniversalApi.Infrastructure
dotnet sln add src/**/*.csproj
```
`10.0.xxx` 需替换为开发机实际安装的 SDK 版本号。

View File

@@ -0,0 +1,8 @@
# deploy
本目录用于放置部署相关文件:
- `docker/`Dockerfile、compose、镜像说明
- `systemd/`service 文件和 Linux 启动说明
后续如需 Nginx 配置、环境变量模板,也统一放在本目录。

View File

@@ -0,0 +1,7 @@
# docker
本目录预留给 Docker 部署资产:
- `Dockerfile`
- `docker-compose.yml`
- 镜像构建与运行说明

View File

@@ -0,0 +1,7 @@
# systemd
本目录预留给 Linux `systemd` 部署资产:
- `MesUniversalApi.Api.service`
- 环境变量说明
- 启停命令说明

View File

@@ -0,0 +1,10 @@
# docs
本目录用于放置新项目内部技术文档,例如:
- action 白名单设计
- 数据源配置样例
- 部署补充说明
- 回归对比样本说明
项目级推进和任务记录仍以 `working1/` 为主。

View File

@@ -0,0 +1,10 @@
# evidence
本目录用于存放验收附件原件,例如:
- 启动截图
- Swagger 页面截图
- 回归响应样本
- PDF、Excel、压测报告
证据索引和结论仍记录在 `working1/05-验收证据.md`

View File

@@ -0,0 +1,11 @@
# src
本目录用于放置新 WebAPI 解决方案中的源码项目:
- `MesUniversalApi.Api`
- `MesUniversalApi.Application`
- `MesUniversalApi.Contracts`
- `MesUniversalApi.Domain`
- `MesUniversalApi.Infrastructure`
`.NET 10 SDK` 安装完成后创建对应项目文件。

View File

@@ -0,0 +1,8 @@
# tests
本目录用于放置测试项目:
- `MesUniversalApi.Tests`
- `MesUniversalApi.IntegrationTests`
测试项目创建时同步纳入解决方案,并按任务矩阵记录验收证据。

View File

@@ -0,0 +1,8 @@
# tools
本目录用于放置迁移过程中的辅助脚本,例如:
- 旧接口资产盘点脚本
- 配置检查脚本
- 回归对比脚本
- 数据样本整理脚本

19
README.md Normal file
View File

@@ -0,0 +1,19 @@
# MES UniversalApi Migration Workspace
This repository tracks the migration preparation workspace for replacing
`MESCommonBase.ashx` with a new `.NET 10` WebAPI service.
## Repository scope
- `working/`: legacy analysis material, flowcharts, and migration route documents
- `working1/`: project management documents, task matrix, evidence, and decisions
- `MesUniversalApi/`: new migration project root for code, tests, deploy assets, tools, and evidence
## Out of scope
The following local content is intentionally not tracked in this repository:
- legacy application source trees outside the migration workspace
- IDE state such as `.vs/` and `.vscode/`
- local credential notes such as `git.txt`
- local archives, binaries, and generated outputs outside the tracked directories

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()

View File

@@ -0,0 +1,266 @@
# 01-项目功能内容
## 1. 项目目标
建设一个替代当前 `MES_Manage/submit/MESCommonBase.ashx` 的通用 WebAPI 服务,运行在 Linux 操作系统上,面向前台提供统一接口,并支持以下数据库:
- SQL Server
- PostgreSQL
- MySQL
新项目不是简单把 `ashx` 改成 WebAPI而是把旧项目中“前台通过 `Type/Name/Param` 驱动 SQL 和存储过程”的模式改造成:
```text
前台调用 WebAPI
-> 后端按 actionId 或 legacy type 找白名单
-> Provider 层选择 SQL Server / PostgreSQL / MySQL
-> 统一参数、统一鉴权、统一响应、统一日志
```
### 1.1 目标 SDK 与目录基线
- 新项目目标 SDK 固定为 `.NET 10`
- 新项目目标 `TargetFramework` 固定为 `net10.0`
- 新代码根目录固定为 `MesUniversalApi/`
- `working/` 仅保留分析资料,`working1/` 仅保留项目管理文档。
-`.NET 10 SDK` 安装到位前,只进行文档、目录、资产盘点和接口梳理,不以 `net9.0` 临时创建正式项目。
## 2. 旧系统核心能力
旧系统入口:
```text
MES_Manage/submit/MESCommonBase.ashx
```
旧系统主要能力:
| 能力 | 旧 Type | 说明 |
| --- | --- | --- |
| Excel 导出 | `2001` | 调用 `MESDownloadExcel.ExcelWebCall.ExcelFile` |
| PDF 导出 | `2002` | 调用 `ExcelWebCall.ExcelFilePdf` |
| Excel 图片或指定扩展名导出 | `2003` | 读取 Form 第一项,生成下载文件 |
| 数据库文件下载 | `2004` | 调用 `DataLink.ExePROCEDURE_Type2004` |
| 文件上传 | `15` | 读取 `Request.Files[0]`,调用 `ExePROCEDURE_Type15` |
| 数据库文件下载 | `16` | 调用 `ExePROCEDURE_Type16` |
| IP 查询 | `4000` | 返回请求 IP |
| 通用 SQL/存储过程调用 | default | 调用 `DataLink.SqlWebCall(type,jsonData,dataobj)` |
`SqlWebCall` 的二次分发能力:
| 能力 | 旧 Type |
| --- | --- |
| 加密、注册、登录 | `8888/5001/5002` |
| 旧协议存储过程 | `1/2/5` |
| 新协议存储过程 | `11/111/12/13/21` |
| 直接 SQL 查询或执行 | `3/4/22/1001/1002/3001` |
| 动态建表导入 | `7` |
## 3. 新项目功能范围
### 3.1 WebAPI 基础能力
新项目必须提供:
- HTTP JSON API。
- OpenAPI/Swagger 文档。
- JWT 鉴权。
- CORS 配置。
- 健康检查接口。
- 统一异常处理中间件。
- 统一响应结构。
- 结构化日志和审计日志。
- Linux 部署能力。
### 3.2 多数据库执行能力
新项目必须支持:
- SQL Server 连接和执行。
- PostgreSQL 连接和执行。
- MySQL 连接和执行。
- 参数绑定。
- 查询返回动态结果集。
- 非查询执行。
- 存储过程或函数调用。
- 分页查询。
- 文件二进制上传和下载。
数据库访问不允许散落在 Controller 中,必须通过 Provider 抽象:
```text
IDatabaseProvider
|-- SqlServerProvider
|-- PostgreSqlProvider
|-- MySqlProvider
```
### 3.3 旧协议兼容能力
新项目应提供短期兼容接口:
```http
POST /api/v1/legacy/execute
```
兼容字段:
- `type`
- `name`
- `param`
- `token`
- `pageSize`
- `pageList`
- `Pagination`
兼容原则:
- 兼容不等于完全放开。
- `name` 必须命中白名单。
- 高风险 Type 默认不开放。
- 旧接口只作为迁移过渡,不作为长期推荐接口。
### 3.4 新 actionId 接口
新业务推荐使用:
```http
POST /api/v1/actions/{actionId}/execute
```
例如:
```http
POST /api/v1/actions/quality.line.query/execute
```
好处:
- 前台不再传 SQL。
- 前台不再传存储过程名。
- 后端可按 actionId 做权限、参数、数据库命令和审计控制。
### 3.5 文件接口
上传:
```http
POST /api/v1/files/{actionId}/upload
```
下载:
```http
GET /api/v1/files/{actionId}/download
```
文件功能要求:
- 支持 multipart 上传。
- 支持文件大小限制。
- 支持扩展名白名单。
- 支持下载文件名 UTF-8 编码。
- 支持跨域读取 `Content-Disposition`
- 支持数据库 BLOB 或外部文件存储。
### 3.6 报表导出
`2001/2002/2003` 迁移为报表 action。
建议接口:
```http
POST /api/v1/reports/{reportId}/export
```
输出格式:
- Excel
- PDF
- 后续可扩展 CSV
## 4. 不在首期范围内的内容
首期不建议做:
- 一次性把所有 SQL Server 存储过程自动翻译成 MySQL/PostgreSQL。
- 继续开放任意 SQL 执行给前台。
- 重写全部前端。
- 重写所有 Excel 模板。
- 建立复杂低代码平台。
首期重点应是:
```text
WebAPI 化 + SQL Server 兼容 + 白名单治理 + Provider 抽象
```
## 5. 旧 Type 到新功能映射
| 旧 Type | 新项目目标功能 | 首期策略 |
| --- | --- | --- |
| `2001` | Excel 导出 | 转报表 action |
| `2002` | PDF 导出 | 转报表 action |
| `2003` | 扩展导出 | 评估后转报表 action |
| `2004` | 文件下载 | 转文件下载 action |
| `15` | 文件上传 | 转文件上传 action |
| `16` | 文件下载 | 转文件下载 action |
| `4000` | IP 查询 | 独立诊断接口 |
| `1/2/5` | 旧协议存储过程 | 兼容但必须白名单 |
| `11/111/12/13/21` | 新协议存储过程 | 优先兼容 |
| `8888/5001/5002` | 加密、注册、登录 | 迁移到 Auth API |
| `3/4/7/22/1001/1002/3001` | 直接 SQL 或建表 | 默认禁用或仅管理端审计开放 |
## 6. 目标质量属性
### 6.1 安全
- 所有执行业务接口默认要求登录。
- action 必须白名单。
- 参数必须按定义校验。
- 禁止前台任意传 SQL。
- 文件上传必须校验大小和类型。
- 连接字符串不进入代码仓库。
### 6.2 可移植
- 运行在 Linux。
- 支持 Docker。
- 支持 systemd。
- 不依赖 IIS。
- 不依赖 .NET Framework。
### 6.3 可观测
- 每次执行有 traceId。
- 每个 action 记录耗时。
- 记录数据库类型和数据源。
- 记录错误摘要。
- 支持健康检查。
### 6.4 可维护
- Controller 只负责 HTTP 协议。
- 业务编排在 Application 层。
- 数据库差异在 Infrastructure Provider 层。
- 任务推进受 `04-任务矩阵.md` 控制。
### 6.5 目录边界
- 不在 `MES_Manage/``02DataLinkMesWork/` 中直接展开新 WebAPI 开发。
- 不把新项目证据散落到旧项目目录。
- 新项目代码、测试、部署、工具和附件统一进入 `MesUniversalApi/`
- `working1/05-验收证据.md` 负责证据索引,原始附件优先放到 `MesUniversalApi/evidence/`
## 7. 首批验收目标
首批可验收功能:
1. WebAPI 项目可在 Linux 启动。
2. Swagger 可打开。
3. JWT 登录和鉴权可用。
4. SQL Server Provider 可执行旧 `Type=11` 查询。
5. Legacy 接口可接收旧格式请求。
6. actionId 接口可执行一个白名单查询。
7. 文件下载可返回正确文件名和二进制。
8. 审计日志可记录用户、action、数据库、耗时、结果。

View File

@@ -0,0 +1,526 @@
# 02-项目程序开发详细步骤
## 1. 开发总路线
项目推荐按以下顺序推进:
```text
准备阶段
-> 建立 WebAPI 骨架
-> 建立通用契约和响应
-> 建立数据库 Provider 抽象
-> 实现 SQL Server Provider
-> 实现 Legacy 兼容入口
-> 加入 actionId 白名单
-> 实现文件上传下载
-> 实现 PostgreSQL Provider
-> 实现 MySQL Provider
-> Linux 部署和回归测试
```
## 2. 技术栈
推荐:
- 语言C#
- 框架ASP.NET Core Web API
- 运行时:.NET 10 LTS
- 数据访问ADO.NET + Provider 策略;必要时配合 Dapper
- SQL Server 驱动:`Microsoft.Data.SqlClient`
- PostgreSQL 驱动:`Npgsql`
- MySQL 驱动:`MySqlConnector`
- 部署Linux + Docker 或 systemd
- 反向代理Nginx
- 文档OpenAPI/Swagger
- 日志Serilog 或 Microsoft.Extensions.Logging + OpenTelemetry
## 3. 新项目结构
建议结构:
```text
MesUniversalApi/
src/
MesUniversalApi.Api/
MesUniversalApi.Application/
MesUniversalApi.Contracts/
MesUniversalApi.Domain/
MesUniversalApi.Infrastructure/
tests/
MesUniversalApi.Tests/
MesUniversalApi.IntegrationTests/
```
当前移植根目录已先行创建为:
```text
MesUniversalApi/
src/
tests/
deploy/
docker/
systemd/
docs/
tools/
evidence/
```
职责:
| 项目 | 职责 |
| --- | --- |
| `Api` | Controller、鉴权、Swagger、过滤器、中间件 |
| `Application` | ActionService、LegacyTypeRouter、FileService、AuthService |
| `Contracts` | 请求和响应 DTO |
| `Domain` | ActionDefinition、DataSourceDefinition、业务模型 |
| `Infrastructure` | 数据库 Provider、日志、配置、JWT |
| `Tests` | 单元测试和集成测试 |
### 3.1 开工前置条件
开始 `T002 新 WebAPI 项目骨架创建` 之前,必须满足以下条件:
1. 开发机已安装 `.NET 10 SDK`
2. `dotnet --list-sdks` 输出中包含 `10.0.x`
3.`MesUniversalApi/` 根目录创建 `global.json`,锁定实际安装的 `10.0.x`
4. 不以 `net9.0` 或更低版本临时创建正式项目骨架。
5. 旧系统目录只用于盘点和对照,新增代码只进入 `MesUniversalApi/`
截至 2026-07-02当前机器仅检测到 `.NET SDK 9.0.311`,因此本轮只完成目录和文档准备,不执行 `net10.0` 项目生成。
## 4. 第一步:创建 WebAPI 骨架
建议命令:
```bash
cd MesUniversalApi
dotnet new globaljson --sdk-version 10.0.xxx
dotnet new sln -n MesUniversalApi
dotnet new webapi -f net10.0 -n MesUniversalApi.Api -o src/MesUniversalApi.Api
dotnet new classlib -f net10.0 -n MesUniversalApi.Application -o src/MesUniversalApi.Application
dotnet new classlib -f net10.0 -n MesUniversalApi.Contracts -o src/MesUniversalApi.Contracts
dotnet new classlib -f net10.0 -n MesUniversalApi.Domain -o src/MesUniversalApi.Domain
dotnet new classlib -f net10.0 -n MesUniversalApi.Infrastructure -o src/MesUniversalApi.Infrastructure
dotnet sln add src/**/*.csproj
```
说明:
- `10.0.xxx` 需替换为开发机实际安装的 `.NET 10 SDK` 版本号。
- 以上命令在 `.NET 10 SDK` 安装完成后执行。
- 当前轮次不以 `net9.0` 代替 `net10.0` 创建正式骨架。
依赖关系:
```text
Api -> Application -> Domain
Api -> Contracts
Application -> Contracts
Application -> Infrastructure abstractions
Infrastructure -> Domain / Contracts
```
## 5. 第二步:统一响应和异常处理
定义响应:
```csharp
public sealed class ApiResponse<T>
{
public string Code { get; init; } = "200";
public string Message { get; init; } = "";
public T? Data { get; init; }
public string TraceId { get; init; } = "";
}
```
分页响应:
```csharp
public sealed class PageResult
{
public IReadOnlyList<IDictionary<string, object?>> Rows { get; init; } = [];
public long Total { get; init; }
public int PageIndex { get; init; }
public int PageSize { get; init; }
}
```
错误中间件:
```text
ExceptionHandlingMiddleware
-> 捕获异常
-> 记录 traceId/user/action
-> 返回 ApiResponse<object>
```
## 6. 第三步:配置数据源
配置模型:
```csharp
public sealed class DataSourceDefinition
{
public string Name { get; init; } = "";
public DatabaseKind Kind { get; init; }
public string ConnectionStringName { get; init; } = "";
}
```
配置示例:
```json
{
"Database": {
"DefaultDataSource": "mes-main",
"DataSources": {
"mes-main": {
"Kind": "SqlServer",
"ConnectionStringName": "MES_SQLSERVER"
},
"mes-pg": {
"Kind": "PostgreSql",
"ConnectionStringName": "MES_POSTGRES"
},
"mes-mysql": {
"Kind": "MySql",
"ConnectionStringName": "MES_MYSQL"
}
}
}
}
```
连接字符串用环境变量:
```bash
ConnectionStrings__MES_SQLSERVER="Server=...;Database=...;User Id=...;Password=...;TrustServerCertificate=True"
ConnectionStrings__MES_POSTGRES="Host=...;Database=...;Username=...;Password=..."
ConnectionStrings__MES_MYSQL="Server=...;Database=...;User ID=...;Password=..."
```
## 7. 第四步:实现 Provider 抽象
核心接口:
```csharp
public interface IDatabaseProvider
{
DatabaseKind Kind { get; }
Task<QueryResult> QueryAsync(DbExecutionContext context, CancellationToken ct);
Task<NonQueryResult> ExecuteAsync(DbExecutionContext context, CancellationToken ct);
Task<ScalarResult> ScalarAsync(DbExecutionContext context, CancellationToken ct);
}
```
执行上下文:
```csharp
public sealed class DbExecutionContext
{
public string DataSourceName { get; init; } = "";
public DatabaseKind DatabaseKind { get; init; }
public CommandKind CommandKind { get; init; }
public string CommandText { get; init; } = "";
public IReadOnlyList<DbParameterValue> Parameters { get; init; } = [];
public PageRequest? Page { get; init; }
public int CommandTimeoutSeconds { get; init; } = 60;
}
```
Provider 实现顺序:
1. `SqlServerProvider`
2. `PostgreSqlProvider`
3. `MySqlProvider`
## 8. 第五步SQL Server Provider
先支持 SQL Server原因
- 当前旧系统以 SQL Server 存储过程为主。
- 先迁移入口,降低一次性风险。
实现能力:
- Text 查询。
- Text 非查询。
- StoredProcedure 查询。
- StoredProcedure 非查询。
- 输出参数。
- DataSet/DataTable 动态 JSON。
- 文件二进制。
关键点:
- 使用 `Microsoft.Data.SqlClient`
- `CommandType.StoredProcedure` 用于旧存储过程。
- 参数名前缀统一由 Provider 处理。
- 命令超时不要照搬 `0`,默认建议 60 秒,可按 action 配置。
## 9. 第六步Legacy 兼容入口
Controller
```http
POST /api/v1/legacy/execute
```
流程:
```text
LegacyController
-> 接收 type/name/param
-> LegacyTypeRouter
-> 白名单检查
-> LegacyParamParser
-> DbExecutionContext
-> Provider 执行
-> ApiResponse
```
优先兼容:
- `11`
- `111`
- `12`
- `13`
- `15`
- `16`
- `2004`
高风险 Type 先禁用:
- `3`
- `4`
- `7`
- `22`
- `1001`
- `1002`
- `3001`
## 10. 第七步actionId 白名单
新增接口:
```http
POST /api/v1/actions/{actionId}/execute
```
动作定义:
```csharp
public sealed class ActionDefinition
{
public string ActionId { get; init; } = "";
public string Module { get; init; } = "";
public bool Enabled { get; init; }
public string[] RequiredRoles { get; init; } = [];
public IReadOnlyList<ParameterDefinition> Parameters { get; init; } = [];
public IReadOnlyDictionary<DatabaseKind, ProviderCommandDefinition> Commands { get; init; }
= new Dictionary<DatabaseKind, ProviderCommandDefinition>();
}
```
执行流程:
```text
ActionController
-> ActionService
-> 读取 ActionDefinition
-> 校验权限
-> 校验参数
-> 根据 dataSource 选择 DatabaseKind
-> 根据 DatabaseKind 选择 CommandDefinition
-> Provider 执行
-> 统一响应
```
## 11. 第八步:文件上传下载
上传接口:
```http
POST /api/v1/files/{actionId}/upload
```
下载接口:
```http
GET /api/v1/files/{actionId}/download
```
开发步骤:
1. 定义文件 action。
2. 实现上传大小限制。
3. 实现扩展名白名单。
4. 实现 MIME 校验。
5. 实现数据库二进制写入。
6. 实现下载响应头。
7. 写文件 hash 校验测试。
二进制类型:
| 数据库 | 类型 |
| --- | --- |
| SQL Server | `varbinary(max)` |
| PostgreSQL | `bytea` |
| MySQL | `longblob` |
## 12. 第九步PostgreSQL Provider
实现内容:
- `NpgsqlConnection`
- 参数转换。
- Text 查询。
- Function 查询。
- 非查询执行。
- `bytea` 文件读写。
- `LIMIT/OFFSET` 分页。
迁移建议:
- SQL Server 查询型存储过程迁移为 PostgreSQL function。
- 输出参数尽量改为返回列或 JSON。
- 不做简单字符串替换,按 action 重写 SQL。
## 13. 第十步MySQL Provider
实现内容:
- `MySqlConnection`
- 参数转换。
- Text 查询。
- `CALL procedure(...)`
- 非查询执行。
- `longblob` 文件读写。
- `LIMIT/OFFSET` 分页。
注意:
- MySQL procedure 输出参数处理与 SQL Server 不同。
- 同一个 action 可为 MySQL 配置单独 SQL。
- 不要求 SQL Server 存储过程原样迁移。
## 14. 第十一步:鉴权与授权
实现:
- JWT 登录。
- `[Authorize]` 保护执行类接口。
- action 级权限。
- 用户角色映射。
- token 过期。
不沿用旧模式:
```text
token 非空才校验
```
新模式:
```text
除登录和健康检查外,默认必须鉴权
```
## 15. 第十二步:日志与审计
每次执行记录:
- traceId
- userId
- clientIp
- actionId
- legacyType
- legacyName
- dataSource
- databaseKind
- commandKind
- durationMs
- rowsAffected
- resultCode
- errorSummary
日志不记录明文密码和大二进制。
## 16. 第十三步Linux 部署
Docker
```bash
dotnet publish -c Release -o publish
docker build -t mes-universal-api:1.0.0 .
docker run -d -p 8080:8080 --env-file .env mes-universal-api:1.0.0
```
systemd
```ini
[Service]
WorkingDirectory=/opt/mes-api
ExecStart=/usr/bin/dotnet /opt/mes-api/MesUniversalApi.Api.dll
Restart=always
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
```
Nginx 反代:
```nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
## 17. 第十四步:测试
单元测试:
- 参数解析。
- 类型转换。
- action 白名单。
- 权限校验。
- 响应包装。
集成测试:
- SQL Server 查询。
- SQL Server 存储过程。
- PostgreSQL 查询。
- MySQL 查询。
- 文件上传下载。
- JWT 成功和失败。
回归测试:
```text
旧 ashx 响应
vs
新 legacy/execute 响应
```
性能测试:
- 大查询。
- 分页。
- 文件上传下载。
- 长存储过程。
- 连接池。
## 18. 首轮开发建议
第一轮只做最小闭环:
1. 安装并验证 `.NET 10 SDK`
2. 新建 WebAPI 项目。
3. 加统一响应。
4. 加 Swagger。
5. 加 SQL Server Provider。
6. 加一个 actionId 查询。
7. 加一个 legacy Type=11 查询。
8. Linux 本地或 Docker 启动。
9. 记录验收证据。

126
working1/03-推进台账.md Normal file
View File

@@ -0,0 +1,126 @@
# 03-推进台账
本台账记录每轮推进做了什么、改了哪些文件、验证了什么、下一步是什么。
## 记录规则
每轮新增一条记录,格式如下:
```text
轮次:
日期:
目标:
完成内容:
改动文件:
验证证据:
遗留问题:
下一步:
```
## R001 - 分析资料整理与项目移植文档框架建立
日期2026-07-02
目标:
- 根据 `working` 中对 `MESCommonBase.ashx` 的分析资料,建立 `working1` 项目移植文档。
- 形成后续可持续推进的 README、功能、开发步骤、台账、任务矩阵、验收证据、决策记录。
完成内容:
- 盘点 `working` 目录已有分析资料。
- 确认 `working1` 目录存在且为空。
- 编写项目文档索引。
- 编写项目功能内容。
- 编写程序开发详细步骤。
- 编写推进台账。
- 编写任务矩阵。
- 编写验收证据。
- 编写决策记录。
改动文件:
- `working1/README.md`
- `working1/01-项目功能内容.md`
- `working1/02-项目程序开发详细步骤.md`
- `working1/03-推进台账.md`
- `working1/04-任务矩阵.md`
- `working1/05-验收证据.md`
- `working1/06-决策记录.md`
验证证据:
- 已读取 `working/MES通用WebAPI多数据库迁移技术路线.md`
- 已列出 `working` 目录文件。
- 已确认 `working1` 原为空目录。
- 已通过文件清单检查确认文档写入。
遗留问题:
- 尚未创建真实 WebAPI 项目代码。
- 尚未盘点所有前台实际调用的旧 Type/Name/Param。
- 尚未连接任何真实数据库。
- 尚未确定首批迁移的业务 action。
下一步:
1. 执行 `T001 旧接口资产盘点`
2. 执行 `T002 新 WebAPI 项目骨架创建`
3. 执行 `T003 数据源配置模型设计`
4. 明确首批 SQL Server 兼容接口。
## R002 - 固定 .NET 10 目标基线并创建新移植目录
日期2026-07-02
目标:
- 固定新项目目标 SDK 为 `.NET 10`
- 完善 `working1` 文档中的移植前准备内容。
- 创建独立的新移植目录,避免与旧系统代码混做。
完成内容:
- 确认目标 SDK 固定为 `.NET 10`,目标框架固定为 `net10.0`
- 确认当前开发机仅安装 `.NET SDK 9.0.311`,尚未具备生成 `net10.0` 项目的条件。
- 在仓库根目录创建 `MesUniversalApi/` 及其 `src/``tests/``deploy/``docs/``tools/``evidence/` 目录骨架。
- 补充 `working1` 中关于目录分工、前置条件、SDK 基线、启动规则和目录边界的文档内容。
- 为新移植目录创建说明文件和占位文件,明确后续代码、测试、部署和证据落点。
改动文件:
- `working1/README.md`
- `working1/01-项目功能内容.md`
- `working1/02-项目程序开发详细步骤.md`
- `working1/03-推进台账.md`
- `working1/04-任务矩阵.md`
- `working1/05-验收证据.md`
- `working1/06-决策记录.md`
- `MesUniversalApi/README.md`
- `MesUniversalApi/.gitignore`
- `MesUniversalApi/src/README.md`
- `MesUniversalApi/tests/README.md`
- `MesUniversalApi/docs/README.md`
- `MesUniversalApi/deploy/README.md`
- `MesUniversalApi/deploy/docker/README.md`
- `MesUniversalApi/deploy/systemd/README.md`
- `MesUniversalApi/tools/README.md`
- `MesUniversalApi/evidence/README.md`
验证证据:
- `dotnet --list-sdks` 输出仅包含 `9.0.311`
- `Get-ChildItem -LiteralPath 'MesUniversalApi' -Recurse -Depth 2` 已确认目录骨架创建成功。
遗留问题:
- `.NET 10 SDK` 尚未安装,当前还不能执行 `T002` 生成 `net10.0` 项目。
- 尚未进行 `T001` 旧接口资产盘点明细输出。
- 尚未建立新项目 git 仓库。
下一步:
1. 安装并验证 `.NET 10 SDK`
2. 执行 `T001 旧接口资产盘点`
3.`MesUniversalApi/` 下执行 `T002 新 WebAPI 项目骨架创建`
4. 补充 `global.json`、解决方案和项目文件。

View File

@@ -0,0 +1,91 @@
# 04-任务矩阵
状态说明:
- `未开始`:尚未实施。
- `进行中`:正在实施。
- `已完成`:已完成并有验收证据。
- `阻塞`:存在外部条件阻塞。
- `取消`:明确不再执行。
## 1. 总任务表
| 编号 | 任务 | 状态 | 优先级 | 验收标准 |
| --- | --- | --- | --- | --- |
| `T000` | 移植前基线准备 | 已完成 | P0 | 目标 SDK 固定为 `.NET 10``working1` 文档补齐;`MesUniversalApi/` 目录骨架创建;当前开发环境约束已记录 |
| `T001` | 旧接口资产盘点 | 未开始 | P0 | 输出旧 Type/Name/Param 调用清单标出文件、SQL、存储过程和高风险接口 |
| `T002` | 新 WebAPI 项目骨架创建 | 未开始 | P0 | 项目可启动Swagger 可访问,健康检查通过 |
| `T003` | 数据源配置模型设计 | 未开始 | P0 | 支持 SQL Server/PostgreSQL/MySQL 三类数据源配置 |
| `T004` | 统一响应与异常处理中间件 | 未开始 | P0 | 所有 JSON 接口返回 `{code,message,data,traceId}` |
| `T005` | JWT 鉴权和 action 授权 | 未开始 | P0 | 未登录不能执行业务接口action 权限可配置 |
| `T006` | Provider 抽象接口 | 未开始 | P0 | 定义 `IDatabaseProvider``DbExecutionContext`、结果模型 |
| `T007` | SQL Server Provider | 未开始 | P0 | 可执行 Text 查询、非查询、StoredProcedure、输出参数 |
| `T008` | Legacy Type=11 兼容 | 未开始 | P0 | 旧格式请求可通过白名单执行 SQL Server 存储过程并返回 JSON |
| `T009` | Legacy Type=111 分页兼容 | 未开始 | P1 | 可追加分页参数并返回 `rows/total` |
| `T010` | Legacy Type=12/13/21 兼容 | 未开始 | P1 | 执行型、DataSet、新结构响应均可用 |
| `T011` | 文件上传 Type=15 迁移 | 未开始 | P1 | multipart 上传成功,支持大小和扩展名校验 |
| `T012` | 文件下载 Type=16/2004 迁移 | 未开始 | P1 | 返回二进制、文件名和正确响应头 |
| `T013` | actionId 白名单执行接口 | 未开始 | P0 | 前台可通过 actionId 执行一个配置动作 |
| `T014` | PostgreSQL Provider | 未开始 | P1 | 可执行 Text 查询、Function、非查询、bytea 文件 |
| `T015` | MySQL Provider | 未开始 | P1 | 可执行 Text 查询、CALL procedure、非查询、longblob 文件 |
| `T016` | 报表导出接口 | 未开始 | P2 | 可按 reportId 导出 Excel/PDF |
| `T017` | 审计日志 | 未开始 | P0 | 记录用户、action、数据库、耗时、结果 |
| `T018` | Linux Docker 部署 | 未开始 | P0 | Docker 容器在 Linux 启动并通过健康检查 |
| `T019` | systemd 部署文档 | 未开始 | P2 | 具备 systemd service 配置和启动说明 |
| `T020` | 集成测试环境 | 未开始 | P1 | docker compose 可启动 API + 三类数据库测试环境 |
| `T021` | 旧接口回归对比 | 未开始 | P0 | 选定旧接口与新接口输出一致或差异有说明 |
| `T022` | 高风险 Type 退场方案 | 未开始 | P0 | `3/4/7/22/1001/1002/3001` 有明确禁用或替代方案 |
| `T023` | OpenAPI 与前台对接文档 | 未开始 | P1 | 前台可按文档调用新接口 |
| `T024` | 性能压测 | 未开始 | P2 | 有查询、分页、文件上传下载压测报告 |
| `T025` | 云仓库初始化与首次提交 | 进行中 | P1 | 云端仓库创建成功;`main` 分支首次提交完成;仓库只包含迁移资料与新移植目录 |
## 2. 当前优先队列
第一批建议执行:
1. `T001` 旧接口资产盘点。
2. `T002` 新 WebAPI 项目骨架创建。
3. `T003` 数据源配置模型设计。
4. `T004` 统一响应与异常处理中间件。
5. `T006` Provider 抽象接口。
6. `T007` SQL Server Provider。
7. `T008` Legacy Type=11 兼容。
## 3. 任务依赖
```text
T000 -> T001/T002/T003/T018/T020
T001 -> T008/T009/T010/T011/T012/T021/T022
T002 -> T003/T004/T005/T006/T018
T006 -> T007/T014/T015
T007 -> T008/T009/T010/T011/T012/T013
T013 -> T014/T015/T023
T017 -> T021/T024
```
## 4. 防重复规则
- 所有新工作必须先在本矩阵中找到或新增任务编号。
- 新增代码、测试、部署文件和工具脚本只进入 `MesUniversalApi/`,不回写到旧系统目录。
- 同一能力不得同时以 legacy 和 actionId 两条线重复开发,除非明确是兼容过渡。
- 已完成任务如果返工,必须在 `03-推进台账.md` 写明原因。
- 验收证据必须写入 `05-验收证据.md` 后才能标记 `已完成`
## 5. 高风险任务说明
### T022 高风险 Type 退场方案
必须处理的旧 Type
- `3`:直接 SQL 查询。
- `4`:直接 SQL 非查询。
- `7`:动态建表,含 DROP/CREATE。
- `22`:直接 SQL 返回新结构。
- `1001`:客户端传 SQL 增删改。
- `1002`:客户端传 SQL 查询。
- `3001`SQL 命令执行入口。
验收标准:
- 每个 Type 都有处置策略:禁用、仅内网管理、替换为 actionId、保留但强审计。
- 默认生产环境不可被普通前台调用。

296
working1/05-验收证据.md Normal file
View File

@@ -0,0 +1,296 @@
# 05-验收证据
本文件记录项目推进过程中的可复核证据包括命令、文件、页面、job_id、report_id、PDF、截图和接口响应。
## 1. 当前资料证据
### E001 - `working` 分析资料清单
日期2026-07-02
命令:
```powershell
Get-ChildItem -LiteralPath 'E:\通讯服务器20251217\通讯服务器20251217\working' -Force |
Select-Object Name,Length,LastWriteTime |
Sort-Object Name
```
关键输出:
```text
MESCommonBase程序梳理.md
MESCommonBase流程图.mmd
MESCommonBase流程图.png
MESCommonBase响应输出与SqlWebCall默认分支详细文档.md
MESCommonBase响应输出与SqlWebCall主流程图.mmd
MESCommonBase响应输出与SqlWebCall主流程图.png
MESCommonBase响应输出与SqlWebCall次流程图.mmd
MESCommonBase响应输出与SqlWebCall次流程图.png
MES通用WebAPI多数据库迁移技术路线.md
MESCommonBase迁移分析资料包.pptx
```
结论:
- `working` 已具备源程序梳理、流程图、默认分支详细文档和迁移技术路线。
### E002 - 技术路线文档读取
日期2026-07-02
命令:
```powershell
Get-Content -LiteralPath 'E:\通讯服务器20251217\通讯服务器20251217\working\MES通用WebAPI多数据库迁移技术路线.md' -TotalCount 80
```
结论:
- 技术路线文档确认了目标Linux WebAPI、MySQL/PostgreSQL/SQL Server、多数据库 Provider、actionId 白名单、旧协议兼容。
### E003 - `working1` 目录状态
日期2026-07-02
命令:
```powershell
Get-ChildItem -LiteralPath 'E:\通讯服务器20251217\通讯服务器20251217\working1' -Force
```
结论:
- 文档创建前 `working1` 为空,可安全写入项目移植文档。
### E004 - `working1` 文档写入
日期2026-07-02
新增文件:
```text
working1/README.md
working1/01-项目功能内容.md
working1/02-项目程序开发详细步骤.md
working1/03-推进台账.md
working1/04-任务矩阵.md
working1/05-验收证据.md
working1/06-决策记录.md
```
验收方式:
```powershell
Get-ChildItem -LiteralPath 'E:\通讯服务器20251217\通讯服务器20251217\working1' -Force |
Select-Object Name,Length,LastWriteTime |
Sort-Object Name
```
预期:
- 以上 7 个文件均存在。
### E005 - 当前开发机 .NET SDK 状态
日期2026-07-02
命令:
```powershell
dotnet --list-sdks
```
关键输出:
```text
9.0.311 [C:\Program Files\dotnet\sdk]
```
结论:
- 目标 SDK 虽已确定为 `.NET 10`,但当前开发机尚未安装 `.NET 10 SDK`
- 本轮只做文档、目录和基线准备,不执行 `net10.0` 项目脚手架创建。
### E006 - 新移植目录 `MesUniversalApi` 骨架创建
日期2026-07-02
命令:
```powershell
Get-ChildItem -LiteralPath 'E:\通讯服务器20251217\通讯服务器20251217\MesUniversalApi' -Recurse -Depth 2 |
Select-Object FullName, PSIsContainer
```
关键输出摘要:
```text
MesUniversalApi\src
MesUniversalApi\tests
MesUniversalApi\deploy
MesUniversalApi\deploy\docker
MesUniversalApi\deploy\systemd
MesUniversalApi\docs
MesUniversalApi\tools
MesUniversalApi\evidence
```
结论:
- 新移植项目目录已独立创建,后续源码、测试、部署和证据文件有明确落点。
## 2. 后续开发证据模板
### API 启动证据模板
任务编号:
命令:
```bash
dotnet run --project src/MesUniversalApi.Api
```
证据:
```text
Now listening on: http://localhost:xxxx
Application started.
```
页面:
```text
http://localhost:xxxx/swagger
```
结论:
### 健康检查证据模板
任务编号:
命令:
```bash
curl http://localhost:8080/health
```
响应:
```json
{
"status": "Healthy"
}
```
结论:
### 数据库连接证据模板
任务编号:
数据库:
命令或接口:
```bash
curl -H "Authorization: Bearer ..." http://localhost:8080/api/v1/admin/datasources/mes-main/test
```
响应:
```json
{
"code": "200",
"message": "",
"data": {
"connected": true,
"databaseKind": "SqlServer"
}
}
```
结论:
### Legacy 接口回归证据模板
任务编号:
旧请求:
```json
{
"type": "11",
"name": "...",
"param": "..."
}
```
旧接口响应摘要:
```text
行数:
字段:
关键值:
```
新接口响应摘要:
```text
行数:
字段:
关键值:
```
结论:
### 文件下载证据模板
任务编号:
命令:
```bash
curl -OJ -H "Authorization: Bearer ..." "http://localhost:8080/api/v1/files/xxx/download?fileId=123"
```
证据:
```text
文件名:
大小:
SHA256
Content-Disposition
```
结论:
### 截图/PDF/报告证据模板
任务编号:
文件:
```text
working1/evidence/...
```
说明:
结论:
## 3. 任务证据跟踪
| 任务编号 | 证据项 | 状态 |
| --- | --- | --- |
| `T000` | SDK 基线与新移植目录骨架 | 已补充 |
| `T001` | 旧接口资产盘点表 | 待补充 |
| `T002` | 新 WebAPI 启动截图或命令输出 | 待补充 |
| `T003` | 三类数据源配置样例 | 待补充 |
| `T007` | SQL Server Provider 连接测试 | 待补充 |
| `T008` | Legacy Type=11 回归响应 | 待补充 |
| `T014` | PostgreSQL Provider 连接测试 | 待补充 |
| `T015` | MySQL Provider 连接测试 | 待补充 |
| `T018` | Linux Docker 启动证据 | 待补充 |

384
working1/06-决策记录.md Normal file
View File

@@ -0,0 +1,384 @@
# 06-决策记录
本文件记录关键技术决策。格式参考 ADR。
状态说明:
- `已采纳`
- `待确认`
- `废弃`
## ADR-001 - 使用 C# 与 ASP.NET Core Web API
状态:已采纳
日期2026-07-02
背景:
- 当前系统为 C#/.NET Framework + `.ashx`
- 目标系统要求运行在 Linux。
- 前台通过 WebAPI 调用。
决策:
- 新项目使用 C#。
- Web 框架使用 ASP.NET Core Web API。
- 运行时优先使用 .NET 10 LTS。
原因:
- 与现有代码和团队知识体系最接近。
- ASP.NET Core 可跨平台运行。
- WebAPI、Swagger、JWT、HealthCheck、日志、Docker 支持成熟。
影响:
-`.ashx` 不直接迁移,改为 Controller。
-`.NET Framework` 依赖需要替换为 .NET 跨平台库。
## ADR-002 - 使用 Provider 策略支持多数据库
状态:已采纳
日期2026-07-02
背景:
- 目标支持 SQL Server、PostgreSQL、MySQL。
- 三种数据库在参数、分页、存储过程、函数、二进制字段、标识符引用方面存在差异。
决策:
- 定义 `IDatabaseProvider` 抽象。
- 分别实现 `SqlServerProvider``PostgreSqlProvider``MySqlProvider`
- 数据库差异只允许在 Provider 层处理。
原因:
- 避免 Controller 和业务层散落数据库判断。
- 允许同一个 actionId 对不同数据库配置不同 SQL 或函数。
- 便于单独测试每种数据库。
影响:
- 首期要设计好 `DbExecutionContext` 和参数模型。
- 不追求三种数据库完全共用同一段 SQL。
## ADR-003 - 使用 actionId 白名单替代前台任意传 SQL/存储过程名
状态:已采纳
日期2026-07-02
背景:
- 旧系统 `Name/name` 可由前台传入。
- `Name/name` 可能是存储过程名、SQL 文本或表名。
- 存在较高安全风险。
决策:
- 新接口以 `actionId` 作为业务动作标识。
- actionId 在后端白名单中配置 SQL、存储过程、参数、权限。
- legacy 接口也必须经过白名单。
原因:
- 降低 SQL 注入和越权执行风险。
- 便于审计。
- 便于多数据库为同一个 action 配置不同实现。
影响:
- 需要建设 action 配置。
- 前台后续要逐步从 `type/name/param` 迁移到 `actionId/parameters`
## ADR-004 - 保留 Legacy 接口作为迁移过渡
状态:已采纳
日期2026-07-02
背景:
- 现有前台可能大量依赖 `Type/Name/Param`
- 一次性改前台和后端风险较大。
决策:
- 新项目提供 `/api/v1/legacy/execute`
- 兼容旧字段,但不允许任意执行。
- 高风险 Type 默认禁用或限制。
原因:
- 降低迁移成本。
- 可逐个业务回归。
- 为前台改造争取时间。
影响:
- 需要实现 `LegacyTypeRouter``LegacyParamParser`
- 需要维护旧 Type 到 action 的映射。
## ADR-005 - SQL Server 优先落地,再扩展 PostgreSQL 和 MySQL
状态:已采纳
日期2026-07-02
背景:
- 当前系统主要依赖 SQL Server。
- 直接同步完成三类数据库会扩大风险。
决策:
- 第一阶段先实现 SQL Server Provider。
- 在 SQL Server 上跑通 legacy 兼容和 actionId。
- 再实现 PostgreSQL Provider。
- 再实现 MySQL Provider。
原因:
- 先保证现有业务迁移可行。
- Provider 抽象可在 SQL Server 实战中校准。
- 后续数据库迁移按 action 渐进完成。
影响:
- 多数据库不是第一天全部完成。
- 任务矩阵需要把三类数据库分开验收。
## ADR-006 - 不自动翻译全部 SQL Server 存储过程
状态:已采纳
日期2026-07-02
背景:
- SQL Server、PostgreSQL、MySQL 语法和过程语义差异明显。
- 自动翻译容易造成隐性错误。
决策:
- 不做全量自动 SQL 翻译。
- 按 actionId 逐个迁移。
- 每个 action 针对不同数据库配置独立 command。
原因:
- 便于测试。
- 便于控制风险。
- 避免假兼容。
影响:
- 迁移周期会按业务 action 分批推进。
- 需要建立回归测试样本。
## ADR-007 - 统一 JSON 响应格式
状态:已采纳
日期2026-07-02
背景:
- 旧接口返回格式混杂:数组 JSON、`result=1/0``{code,message,data}`、文本、空响应、`NULL`
决策:
- 新 JSON API 统一返回:
```json
{
"code": "200",
"message": "",
"data": {},
"traceId": ""
}
```
原因:
- 前台处理更简单。
- 错误更清晰。
- 审计和排障更方便。
影响:
- legacy 兼容接口可短期保留旧格式,但新接口必须统一格式。
## ADR-008 - 强制鉴权而不是 token 非空才校验
状态:已采纳
日期2026-07-02
背景:
- 旧代码中很多方法只在 `token` 非空时校验。
- 不传 token 时可能继续执行。
决策:
- 新系统除登录、健康检查等公开接口外,默认都要求 JWT 鉴权。
- action 再做权限校验。
原因:
- 明确安全边界。
- 防止前台漏传 token 导致绕过。
- 便于统一审计用户身份。
影响:
- 前台必须接入登录和 Bearer Token。
- legacy 接口也默认要求 JWT。
## ADR-009 - 高风险 Type 默认禁用或治理后开放
状态:已采纳
日期2026-07-02
背景:
旧系统以下 Type 风险较高:
- `3`
- `4`
- `7`
- `22`
- `1001`
- `1002`
- `3001`
决策:
- 生产环境默认禁用。
- 如确需保留,必须在管理端、内网、强审计、白名单条件下开放。
原因:
- 这些 Type 涉及直接 SQL、动态建表、命令执行。
- 多数据库迁移后风险会扩大。
影响:
- 需要逐个找替代 action。
- 需要和前台确认是否仍在使用。
## ADR-010 - 文件存储优先支持数据库二进制,后续可扩展对象存储
状态:待确认
日期2026-07-02
背景:
- 旧系统文件上传下载通过数据库存取 byte[]。
- 新系统需要兼容旧业务。
- 大文件放数据库可能影响性能和备份。
决策:
- 首期兼容数据库二进制。
- 设计 `FileService` 时预留对象存储或文件系统扩展。
原因:
- 首期降低迁移成本。
- 后续可按文件大小和业务类型分流。
待确认:
- 是否有大文件场景。
- 是否已有对象存储、NAS 或 MinIO。
- 是否要求文件加密和病毒扫描。
## ADR-011 - 新移植代码目录独立使用 `MesUniversalApi`
状态:已采纳
日期2026-07-02
背景:
- 当前仓库根目录同时包含旧 WebSite、旧类库、分析资料和管理文档。
- 如果直接在旧系统目录中混合创建新 WebAPI 代码,后续盘点、验收和回归会变得混乱。
决策:
- 新移植项目统一放在 `MesUniversalApi/`
- `working/` 继续保留旧系统分析资料。
- `working1/` 继续保留项目管理文档。
原因:
- 代码边界清晰。
- 旧系统可继续作为回归基线。
- 便于后续单独初始化 git 仓库、部署和验收。
影响:
- `T002` 及后续所有新代码、测试、部署和工具脚本都从 `MesUniversalApi/` 开始。
- 不在 `MES_Manage/``02DataLinkMesWork/` 等旧目录中直接展开新 WebAPI 开发。
## ADR-012 - 在 .NET 10 SDK 到位前只做基线准备,不用低版本临时落地
状态:已采纳
日期2026-07-02
背景:
- 新项目目标运行时已确定为 `.NET 10`
- 当前开发机仅检测到 `.NET SDK 9.0.311`
决策:
-`.NET 10 SDK` 安装到位前,只做文档、目录、任务、证据和资产盘点准备。
- 不以 `net9.0` 或更低版本临时创建正式项目骨架。
-`.NET 10 SDK` 安装后,再在 `MesUniversalApi/` 中创建 `global.json` 和正式项目文件。
原因:
- 避免先建低版本项目再返工升级。
- 避免把环境缺口误判为代码问题。
- 保持目标技术基线一致。
影响:
- 当前轮次不会生成可编译的 `net10.0` 项目代码。
- `T002` 的执行前置条件明确依赖 `.NET 10 SDK` 安装完成。
## ADR-013 - 云仓库只提交迁移资料和新移植目录
状态:已采纳
日期2026-07-02
背景:
- 当前本地根目录同时包含旧系统源码、IDE 状态、二进制产物、压缩包、分析资料和新移植目录。
- 用户要求把当前迁移工作提交到云仓库。
决策:
- 云仓库只跟踪 `working/``working1/``MesUniversalApi/`
- 不把旧系统全量源码、`.vs/`、本地压缩包、凭据文件和其他本地产物推送到云仓库。
原因:
- 保持仓库聚焦于迁移工作本身。
- 降低无关大文件和本地状态污染。
- 避免把本地凭据文件一并入库。
影响:
- 云仓库是迁移工作仓库,不是旧系统完整备份仓库。
- 如果后续确需纳入某部分旧代码,应单独评估并更新范围控制规则。

86
working1/README.md Normal file
View File

@@ -0,0 +1,86 @@
# MES 通用 WebAPI 多数据库迁移项目文档索引
本文档目录用于承接 `working` 中对 `MESCommonBase.ashx``DataLink.SqlWebCall`、响应输出、流程图和多数据库迁移路线的分析,形成后续项目推进时可持续维护的项目移植资料。
## 文档清单
| 编号 | 文档 | 用途 |
| --- | --- | --- |
| 01 | [01-项目功能内容.md](./01-项目功能内容.md) | 定义新项目要实现的功能范围、旧系统能力映射和目标边界 |
| 02 | [02-项目程序开发详细步骤.md](./02-项目程序开发详细步骤.md) | 按阶段说明从建项目到多数据库落地的开发方法 |
| 03 | [03-推进台账.md](./03-推进台账.md) | 记录每轮做了什么、改了哪些文件、验证了什么、下一步是什么 |
| 04 | [04-任务矩阵.md](./04-任务矩阵.md) | 汇总任务编号、状态、验收标准,防止重复做 |
| 05 | [05-验收证据.md](./05-验收证据.md) | 存放命令、文件、截图、报告、页面、job_id、report_id 等证据索引 |
| 06 | [06-决策记录.md](./06-决策记录.md) | 记录关键技术决策和原因,避免后续重复争论 |
## 目录分工
- `working`:保留旧系统分析资料、流程图、路线文档和演示材料,不放新项目代码。
- `working1`:保留项目管理文档、任务矩阵、推进台账、验收证据和决策记录。
- `MesUniversalApi`:新移植项目根目录,承载后续 WebAPI 源码、测试、部署文件、工具脚本和证据附件。
## 来源资料
本目录内容基于 `working` 目录中的分析资料整理:
- `working/MESCommonBase程序梳理.md`
- `working/MESCommonBase流程图.mmd`
- `working/MESCommonBase流程图.png`
- `working/MESCommonBase响应输出与SqlWebCall默认分支详细文档.md`
- `working/MESCommonBase响应输出与SqlWebCall主流程图.mmd`
- `working/MESCommonBase响应输出与SqlWebCall主流程图.png`
- `working/MESCommonBase响应输出与SqlWebCall次流程图.mmd`
- `working/MESCommonBase响应输出与SqlWebCall次流程图.png`
- `working/MES通用WebAPI多数据库迁移技术路线.md`
- `working/MESCommonBase迁移分析资料包.pptx`
## 后续推进规则
1. 新增任务先写入 `04-任务矩阵.md`,获得编号后再实施。
2. 每轮完成后更新 `03-推进台账.md`,记录改动、验证和下一步。
3. 所有可复核结果写入 `05-验收证据.md`,包括命令、文件路径、截图、报告和接口响应。
4. 影响架构、技术选型、安全边界、数据兼容的决定写入 `06-决策记录.md`
5. 需求范围、功能边界或旧 Type 映射变化时,同步更新 `01-项目功能内容.md`
6. 开发步骤、脚手架、部署或测试方法变化时,同步更新 `02-项目程序开发详细步骤.md`
## 当前环境基线
- 目标 SDK`.NET 10`
- 目标 `TargetFramework``net10.0`
- 新移植代码目录:`MesUniversalApi/`
- 当前开发机实际 SDK 状态2026-07-02仅检测到 `.NET SDK 9.0.311`
- 本轮处理原则:先完成文档、目录、任务和证据基线准备;待 `.NET 10 SDK` 安装完成后,再在 `MesUniversalApi/` 内执行 `T002` 创建项目骨架并锁定 `global.json`
## 当前推荐主线
```text
资产盘点
-> ASP.NET Core WebAPI 项目骨架
-> SQL Server Provider 优先兼容
-> Legacy Type 兼容入口
-> actionId 白名单
-> PostgreSQL Provider
-> MySQL Provider
-> 高风险 Type 退场
-> Linux 部署与验收
```
## 当前状态
截至本目录创建时,已完成:
-`MESCommonBase.ashx` 程序梳理。
- 响应输出与 `DataLink.SqlWebCall` 默认分支详细分析。
- 主流程图和次流程图。
- 通用 WebAPI 多数据库迁移技术路线。
- 本目录的项目移植管理文档框架。
- 目标 SDK 已固定为 `.NET 10`
- 新移植目录 `MesUniversalApi/` 已创建基础骨架。
- 当前开发机尚未安装 `.NET 10 SDK`,还不能开始 `net10.0` 项目脚手架生成。
下一步建议:
- 安装并验证 `.NET 10 SDK`
-`04-任务矩阵.md``T001 旧接口资产盘点` 开始推进。
-`MesUniversalApi/` 下执行 `T002 新 WebAPI 项目骨架创建`
- 明确首批迁移 Type 和首批业务 action。