commit 705aec6ab2b4d896496f17dc68c13a544810dd70 Author: wangdequan Date: Thu Jul 2 10:38:13 2026 +0800 chore: initialize migration workspace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df96d6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +* +!/.gitignore +!/README.md +!/MesUniversalApi/ +!/MesUniversalApi/** +!/working/ +!/working/** +!/working1/ +!/working1/** diff --git a/MesUniversalApi/.gitignore b/MesUniversalApi/.gitignore new file mode 100644 index 0000000..71a5214 --- /dev/null +++ b/MesUniversalApi/.gitignore @@ -0,0 +1,11 @@ +bin/ +obj/ +.vs/ +TestResults/ +artifacts/ +publish/ +*.user +*.suo +*.log +.env +.env.* diff --git a/MesUniversalApi/README.md b/MesUniversalApi/README.md new file mode 100644 index 0000000..55df44f --- /dev/null +++ b/MesUniversalApi/README.md @@ -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 版本号。 diff --git a/MesUniversalApi/deploy/README.md b/MesUniversalApi/deploy/README.md new file mode 100644 index 0000000..55777d4 --- /dev/null +++ b/MesUniversalApi/deploy/README.md @@ -0,0 +1,8 @@ +# deploy + +本目录用于放置部署相关文件: + +- `docker/`:Dockerfile、compose、镜像说明 +- `systemd/`:service 文件和 Linux 启动说明 + +后续如需 Nginx 配置、环境变量模板,也统一放在本目录。 diff --git a/MesUniversalApi/deploy/docker/README.md b/MesUniversalApi/deploy/docker/README.md new file mode 100644 index 0000000..cd289d5 --- /dev/null +++ b/MesUniversalApi/deploy/docker/README.md @@ -0,0 +1,7 @@ +# docker + +本目录预留给 Docker 部署资产: + +- `Dockerfile` +- `docker-compose.yml` +- 镜像构建与运行说明 diff --git a/MesUniversalApi/deploy/systemd/README.md b/MesUniversalApi/deploy/systemd/README.md new file mode 100644 index 0000000..04ae9ce --- /dev/null +++ b/MesUniversalApi/deploy/systemd/README.md @@ -0,0 +1,7 @@ +# systemd + +本目录预留给 Linux `systemd` 部署资产: + +- `MesUniversalApi.Api.service` +- 环境变量说明 +- 启停命令说明 diff --git a/MesUniversalApi/docs/README.md b/MesUniversalApi/docs/README.md new file mode 100644 index 0000000..986fac3 --- /dev/null +++ b/MesUniversalApi/docs/README.md @@ -0,0 +1,10 @@ +# docs + +本目录用于放置新项目内部技术文档,例如: + +- action 白名单设计 +- 数据源配置样例 +- 部署补充说明 +- 回归对比样本说明 + +项目级推进和任务记录仍以 `working1/` 为主。 diff --git a/MesUniversalApi/evidence/README.md b/MesUniversalApi/evidence/README.md new file mode 100644 index 0000000..1da89cf --- /dev/null +++ b/MesUniversalApi/evidence/README.md @@ -0,0 +1,10 @@ +# evidence + +本目录用于存放验收附件原件,例如: + +- 启动截图 +- Swagger 页面截图 +- 回归响应样本 +- PDF、Excel、压测报告 + +证据索引和结论仍记录在 `working1/05-验收证据.md`。 diff --git a/MesUniversalApi/src/README.md b/MesUniversalApi/src/README.md new file mode 100644 index 0000000..776a85f --- /dev/null +++ b/MesUniversalApi/src/README.md @@ -0,0 +1,11 @@ +# src + +本目录用于放置新 WebAPI 解决方案中的源码项目: + +- `MesUniversalApi.Api` +- `MesUniversalApi.Application` +- `MesUniversalApi.Contracts` +- `MesUniversalApi.Domain` +- `MesUniversalApi.Infrastructure` + +在 `.NET 10 SDK` 安装完成后创建对应项目文件。 diff --git a/MesUniversalApi/tests/README.md b/MesUniversalApi/tests/README.md new file mode 100644 index 0000000..1cba032 --- /dev/null +++ b/MesUniversalApi/tests/README.md @@ -0,0 +1,8 @@ +# tests + +本目录用于放置测试项目: + +- `MesUniversalApi.Tests` +- `MesUniversalApi.IntegrationTests` + +测试项目创建时同步纳入解决方案,并按任务矩阵记录验收证据。 diff --git a/MesUniversalApi/tools/README.md b/MesUniversalApi/tools/README.md new file mode 100644 index 0000000..792183d --- /dev/null +++ b/MesUniversalApi/tools/README.md @@ -0,0 +1,8 @@ +# tools + +本目录用于放置迁移过程中的辅助脚本,例如: + +- 旧接口资产盘点脚本 +- 配置检查脚本 +- 回归对比脚本 +- 数据样本整理脚本 diff --git a/README.md b/README.md new file mode 100644 index 0000000..dddb008 --- /dev/null +++ b/README.md @@ -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 diff --git a/working/MESCommonBase响应输出与SqlWebCall主流程图.mmd b/working/MESCommonBase响应输出与SqlWebCall主流程图.mmd new file mode 100644 index 0000000..01c54ad --- /dev/null +++ b/working/MESCommonBase响应输出与SqlWebCall主流程图.mmd @@ -0,0 +1,36 @@ +%% MESCommonBase 响应输出与 SqlWebCall 主流程图 +flowchart TD + A([MESCommonBase.ashx
ProcessRequest]) + B[解析请求并取得 type
JSON body / jsonobj / Request param] + C{switch(type)} + + A --> B --> C + + C -->|2001/2002/2003/2004/16| D[文件下载类分支
生成或读取 bytes/fileName/extension] + C -->|15| E[文件上传分支
读取 Request.Files[0]
调用 ExePROCEDURE_Type15] + C -->|4000| F[IP 查询分支
读取 ServerVariables / UserHostAddress] + C -->|default| G[普通业务分支
DataLink.SqlWebCall(type,jsonData,dataobj)] + + D --> D1{bytes 是否有效?} + D1 -->|是| R1[二进制下载响应
Content-Type: application/octet-stream
Content-Disposition: attachment
BinaryWrite + Flush + End/Close] + D1 -->|否
主要见 Type=16| R0[直接 return
可能为空响应] + + E --> R2[JSON 文本响应
Response.Write(responseText)
常见 result=1 / result=0] + F --> R3[文本 IP 响应
Response.Write(userIP)
Content-Type 仍可能是 application/json] + G --> G1[SqlWebCall 返回字符串
数组 JSON / code-message-data / 空字符串] + G1 --> R2 + + A -.外层 catch(Exception err).-> X[异常兜底
不记录异常
Response.Write(responseText)
初始 responseText = NULL] + X --> END([请求结束]) + R1 --> END + R0 --> END + R2 --> END + R3 --> END + + subgraph WARN[主流程注意点] + W1[Response.End 位于 try 内
可能触发 ThreadAbortException] + W2[响应格式不统一
二进制 / JSON / 文本 / NULL / 空响应] + W3[default 分支执行能力强
依赖 SqlWebCall 内部 Type 二次分发] + end + + END --> WARN diff --git a/working/MESCommonBase响应输出与SqlWebCall主流程图.png b/working/MESCommonBase响应输出与SqlWebCall主流程图.png new file mode 100644 index 0000000..3a5679f Binary files /dev/null and b/working/MESCommonBase响应输出与SqlWebCall主流程图.png differ diff --git a/working/MESCommonBase响应输出与SqlWebCall次流程图.mmd b/working/MESCommonBase响应输出与SqlWebCall次流程图.mmd new file mode 100644 index 0000000..031cb17 --- /dev/null +++ b/working/MESCommonBase响应输出与SqlWebCall次流程图.mmd @@ -0,0 +1,53 @@ +%% DataLink.SqlWebCall 默认分支次流程图 +flowchart TD + A([MESCommonBase default 分支]) + B[DataLink.SqlWebCall(type,jsonData,dataobj)] + C{initSystemIsOk?} + D[InitSystemReg
读取 Web.config appSettings ConnectionString
initSystemIsOk = true] + E{switch(type)} + + A --> B --> C + C -->|否| D --> E + C -->|是| E + + E -->|8888| L1[GetEncryptStr
读取 Name/name 并加密] + E -->|5001| L2[ExePROCEDURE_Type11_AddUser
Password MD5
执行 Name 指定存储过程] + E -->|5002| L3[ExePROCEDURE_Type11_Login
解密密码并比对
成功写入 token] + + E -->|1/2/5| O1[旧协议存储过程
对象: jsonobj dataobj
Param 为拼接字符串] + O1 --> O2[SQLCommon.GetCmdParam
按 & / = / | 拆参数
支持 output 参数] + O2 --> DBP[SQLCommon.ExecuteStoredProcedure] + + E -->|11/111/12/13/21| N1[新协议存储过程
对象: JsonData jsonData
Param 为 JSON 数组字符串] + N1 --> N2[GetString_JsonData
读取 Type/Name/Param/token/pageSize/pageList] + N2 --> T{token 非空?} + T -->|是| T1[CheckToken
权限管理_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 / 建表导入 / 命令执行类
Name/name 可能是 SQL 文本或表名] + S1 --> DBS[SQLCommon.ExecuteDataTable / ExecuteDataset
ExecuteInsertMesWork / ExecuteSelectMesWork
或 DbCallType1003_SqlCmd.SqlExec] + + L1 --> R1[返回加密字符串] + L2 --> DBP + L3 --> DBP + DBP --> R2[返回结果
DataTable JSON / DataSet JSON
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 多数是非空才校验
不传 token 常见路径不拒绝] + K2[客户端可控制 Name/name
存储过程名 / SQL 文本 / 表名] + K3[CommandTimeout = 0
长 SQL 或存储过程可能长期阻塞] + K4[未支持 Type 返回空字符串
异常常返回 result=0] + end + + END --> RISK diff --git a/working/MESCommonBase响应输出与SqlWebCall次流程图.png b/working/MESCommonBase响应输出与SqlWebCall次流程图.png new file mode 100644 index 0000000..69d0bb4 Binary files /dev/null and b/working/MESCommonBase响应输出与SqlWebCall次流程图.png differ diff --git a/working/MESCommonBase响应输出与SqlWebCall默认分支详细文档.md b/working/MESCommonBase响应输出与SqlWebCall默认分支详细文档.md new file mode 100644 index 0000000..17db07f --- /dev/null +++ b/working/MESCommonBase响应输出与SqlWebCall默认分支详细文档.md @@ -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 / 3001:SQL、建表导入、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) + | + |-- 遍历数组 + |-- name:SqlParameter 名 + |-- 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 白名单、异常日志和响应格式统一。 diff --git a/working/MESCommonBase流程图.mmd b/working/MESCommonBase流程图.mmd new file mode 100644 index 0000000..13be0a3 --- /dev/null +++ b/working/MESCommonBase流程图.mmd @@ -0,0 +1,58 @@ +%% MESCommonBase.ashx 主流程图 +flowchart TD + START([HTTP 请求
MES_Manage/submit/MESCommonBase.ashx]) + READ[ProcessRequest
设置 ContentType=application/json
读取 Request.InputStream] + PARSE1[JsonMapper.ToObject(stream)
尝试读取 Type / type] + PARSE2[JavaScriptSerializer.Deserialize<jsonobj>(stream)
成功后用 dataobj.Type 覆盖 type] + PARAM{dataobj == null?} + PARAMYES[读取 Request["param"]
再次按 JSON 解析 Type / type] + SWITCH{switch(type)} + + START --> READ --> PARSE1 --> PARSE2 --> PARAM + PARAM -->|是| PARAMYES --> SWITCH + PARAM -->|否| SWITCH + + SWITCH -->|2001| T2001[Excel 导出
ExcelWebCall.ExcelFile(jsonData)
生成 bytes/fileName/extension] + SWITCH -->|2002| T2002[PDF 导出
ExcelWebCall.ExcelFilePdf(jsonData)
Excel 转 PDF] + SWITCH -->|2003| T2003[合成 Excel 图片/指定扩展名导出
读取 Form 第一项 dataimg[0]
ExcelWebCall.ExcelFile(jsonData)] + SWITCH -->|2004| T2004[数据库文件下载
DataLink.ExePROCEDURE_Type2004(jsonData)
返回 bytes/fileName/fileExtension] + SWITCH -->|15| T15[文件上传
读取 Request.Files[0]
拆分 name/suffix/bytes
DataLink.ExePROCEDURE_Type15] + SWITCH -->|16| T16[数据库文件下载
DataLink.ExePROCEDURE_Type16(jsonData)
返回 fileName/suffix/bytes] + SWITCH -->|4000| T4000[IP 查询
读取 ServerVariables / UserHostAddress] + SWITCH -->|default| DEFAULT[通用业务调用
DataLink.SqlWebCall(type,jsonData,dataobj)] + + T2001 --> DOWNLOAD[二进制下载响应
Content-Type=application/octet-stream
Content-Disposition=attachment
BinaryWrite + Flush/End] + T2002 --> DOWNLOAD + T2003 --> DOWNLOAD + T2004 --> DOWNLOAD + T16 --> BYTESNULL{bytes == null?} + BYTESNULL -->|是| RETURNEMPTY[直接 return
不写响应体] + BYTESNULL -->|否| DOWNLOAD + + T15 --> JSONRESP[JSON 文本响应
Response.Write(responseText)] + T4000 --> IPRESP[文本响应
Response.Write(userIP)] + DEFAULT --> SQLWEB + + subgraph SQLWEB[DataLink.SqlWebCall 默认分支] + S1[初始化连接
InitSystemReg 读取 Web.config: ConnectionString] + S2{按 type 再分发} + S3[登录/加密
8888: GetEncryptStr
5001: AddUser/改密
5002: Login/生成 token] + S4[旧协议存储过程
1/2/5
Param 为拼接字符串] + S5[新协议存储过程
11/111/12/13/21
Param 为 JSON 数组字符串] + S6[直接 SQL / 导入
1001/1002/3/4/7/22/3001] + S7[返回 JSON
数组 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[异常处理
catch(Exception err)
Response.Write(responseText)] + CATCH --> END diff --git a/working/MESCommonBase流程图.png b/working/MESCommonBase流程图.png new file mode 100644 index 0000000..5cc070c Binary files /dev/null and b/working/MESCommonBase流程图.png differ diff --git a/working/MESCommonBase程序梳理.md b/working/MESCommonBase程序梳理.md new file mode 100644 index 0000000..fdc6918 --- /dev/null +++ b/working/MESCommonBase程序梳理.md @@ -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(stream) + | 成功则用 dataobj.Type 覆盖 type + | + |-- 如果 dataobj == null + | 尝试读取 HttpContext.Current.Request["param"] + | 再用 JsonMapper 解析并读取 Type/type + | + |-- switch(type) + |-- 2001/2002/2003/2004:生成并下载文件 + |-- 15:上传文件到数据库或存储过程 + |-- 16:从数据库或存储过程取文件并下载 + |-- 4000:返回请求 IP + |-- default:DataLink.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(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 导出、文件上传和文件下载。但它当前把大量执行权交给客户端请求参数,且鉴权、白名单、异常日志和文件安全控制都比较弱。若该接口面向非可信调用方,应优先补齐入口鉴权和可执行对象白名单,再逐步拆分成明确的业务接口。 diff --git a/working/MESCommonBase迁移分析资料包.pptx b/working/MESCommonBase迁移分析资料包.pptx new file mode 100644 index 0000000..4701254 Binary files /dev/null and b/working/MESCommonBase迁移分析资料包.pptx differ diff --git a/working/MES通用WebAPI多数据库迁移技术路线.md b/working/MES通用WebAPI多数据库迁移技术路线.md new file mode 100644 index 0000000..8f8434b --- /dev/null +++ b/working/MES通用WebAPI多数据库迁移技术路线.md @@ -0,0 +1,1326 @@ +# MES 通用 WebAPI 多数据库迁移技术路线 + +目标:参照当前 `MESCommonBase.ashx + DataLink.SqlWebCall` 项目,重新设计一个可运行在 Linux 上、以 WebAPI 对前台提供服务、同时支持 MySQL、PostgreSQL、SQL Server 的通用服务端项目。 + +说明:用户提到的 `PostPreSQL` 下文按 `PostgreSQL` 理解;`SqlServerd` 下文按 `SQL Server` 理解。 + +生成时间:2026-07-02 + +## 1. 当前项目迁移背景 + +当前项目的核心模式是: + +```text +前台请求 MESCommonBase.ashx + | + |-- 请求体或 param 中带 Type / Name / Param + | + |-- MESCommonBase.ashx 按 Type 做第一层分发 + |-- 文件下载:2001/2002/2003/2004/16 + |-- 文件上传:15 + |-- IP 查询:4000 + |-- 其他:DataLink.SqlWebCall(type,jsonData,dataobj) + | + |-- SqlWebCall 再按 Type 做第二层数据库能力分发 + |-- 存储过程 + |-- SQL 查询 + |-- SQL 增删改 + |-- 登录与 token + |-- 分页 + |-- 文件二进制 +``` + +当前模式的优点: + +- 一个入口承载大量业务。 +- 前台只需要传 `Type/Name/Param` 即可触发不同业务。 +- 对 SQL Server 存储过程和动态 SQL 的支持比较直接。 + +当前模式的主要问题: + +- 客户端可控制 `Name/name`,也就是存储过程名、SQL 文本或表名。 +- token 不是入口层强制校验,很多方法是“token 非空才校验”。 +- 响应格式不统一,有二进制、数组 JSON、`{code,message,data}`、文本 IP、空响应、`NULL`。 +- `ashx` 和 `.NET Framework` 不适合 Linux 原生运行。 +- SQL Server 语义强耦合,迁移到 MySQL/PostgreSQL 时不能直接复用所有 SQL 和存储过程。 + +新项目的设计原则: + +- 兼容旧协议,但不继续扩大旧协议风险。 +- WebAPI 化,运行在 Linux。 +- 把数据库差异收敛到 Provider 策略层。 +- 用白名单动作替代“前台任意传 SQL/存储过程名”。 +- 统一鉴权、统一响应、统一日志、统一错误码。 +- 对现有业务分阶段迁移,而不是一次性重写所有存储过程。 + +## 2. 技术选型建议 + +### 2.1 运行时 + +推荐: + +- `.NET 10 LTS` +- `ASP.NET Core Web API` +- Linux + Kestrel +- Docker 或 systemd 部署 +- Nginx 作为可选反向代理 + +原因: + +- .NET 10 是当前 LTS 版本,官方支持到 2028-11-14。 +- .NET 8 LTS 在 2026-11-10 结束支持,不适合作为新项目长期基线。 +- ASP.NET Core Web API 可跨平台运行,适合 Linux。 + +如果组织内部暂时无法升级 .NET 10,可用 `.NET 8` 做短期过渡,但路线中应明确升级窗口。 + +### 2.2 数据访问方式 + +推荐主路径: + +- 核心动态执行层:`ADO.NET + 自定义 Provider 策略` +- 简化参数和动态结果:可辅助使用 `Dapper` +- 元数据、用户、权限、白名单配置:可使用 `EF Core` + +不建议只用 EF Core 完成全部迁移,原因: + +- 当前项目大量能力是动态 SQL、动态存储过程、动态结果集、输出参数、二进制文件。 +- EF Core 更适合实体模型 CRUD,不适合作为“通用 SQL/存储过程网关”的唯一执行层。 +- 多数据库下存储过程、函数、输出参数和多结果集差异较大,必须有 provider-specific 策略。 + +推荐驱动: + +| 数据库 | 推荐驱动 | 用途 | +| --- | --- | --- | +| SQL Server | `Microsoft.Data.SqlClient` | SQL Server ADO.NET Provider | +| PostgreSQL | `Npgsql` | PostgreSQL ADO.NET Provider | +| MySQL/MariaDB | `MySqlConnector` | MySQL ADO.NET Provider | + +### 2.3 API 风格 + +推荐: + +- Controller 风格 WebAPI,便于分组、版本化、过滤器、OpenAPI。 +- 对外使用 REST-ish 接口,不继续暴露 `.ashx`。 +- 保留一个 `legacy` 兼容接口接收旧 `Type/Name/Param` 请求。 +- 新业务使用明确的 action id,例如 `quality.queryLineData`,不让前台直接传 SQL。 + +## 3. 总体架构 + +建议采用分层架构: + +```text +Frontend + | + | HTTP/JSON, multipart/form-data, file download + v +ASP.NET Core WebAPI + | + |-- Controllers + | |-- AuthController + | |-- LegacyController + | |-- ActionController + | |-- FileController + | |-- HealthController + | + |-- Application Layer + | |-- ActionService + | |-- LegacyTypeRouter + | |-- FileService + | |-- AuthService + | + |-- Domain / Contract + | |-- ActionDefinition + | |-- DbCommandRequest + | |-- DbCommandResult + | |-- ParameterDefinition + | |-- UnifiedResponse + | + |-- Infrastructure + |-- DatabaseProviderFactory + |-- IDatabaseProvider + |-- SqlServerProvider + |-- PostgreSqlProvider + |-- MySqlProvider + |-- ConnectionStringResolver + |-- AuditLogger +``` + +核心思想: + +- Controller 不直接拼 SQL,不直接访问数据库。 +- `LegacyTypeRouter` 负责兼容旧 Type。 +- `ActionService` 负责执行已登记的动作。 +- `IDatabaseProvider` 负责屏蔽 MySQL/PostgreSQL/SQL Server 差异。 +- `ActionDefinition` 是白名单元数据,决定可执行什么 SQL 或存储过程。 + +## 4. 推荐项目结构 + +```text +MesUniversalApi/ + src/ + MesUniversalApi.Api/ + Controllers/ + AuthController.cs + LegacyController.cs + ActionsController.cs + FilesController.cs + HealthController.cs + Program.cs + appsettings.json + appsettings.Development.json + + MesUniversalApi.Application/ + Services/ + ActionService.cs + LegacyTypeRouter.cs + FileService.cs + AuthService.cs + Mapping/ + LegacyParamParser.cs + RequestNormalizer.cs + Validation/ + ActionRequestValidator.cs + + MesUniversalApi.Contracts/ + Requests/ + ExecuteActionRequest.cs + LegacyExecuteRequest.cs + DbParameterDto.cs + Responses/ + ApiResponse.cs + QueryResultResponse.cs + FileDownloadDescriptor.cs + Enums/ + DatabaseKind.cs + CommandKind.cs + + MesUniversalApi.Domain/ + Models/ + ActionDefinition.cs + ParameterDefinition.cs + DataSourceDefinition.cs + UserSession.cs + + MesUniversalApi.Infrastructure/ + Database/ + IDatabaseProvider.cs + DatabaseProviderFactory.cs + SqlServer/ + SqlServerProvider.cs + SqlServerDialect.cs + PostgreSql/ + PostgreSqlProvider.cs + PostgreSqlDialect.cs + MySql/ + MySqlProvider.cs + MySqlDialect.cs + Security/ + JwtTokenService.cs + Observability/ + AuditLogger.cs + + tests/ + MesUniversalApi.Tests/ + MesUniversalApi.IntegrationTests/ +``` + +## 5. WebAPI 接口规划 + +### 5.1 认证接口 + +```http +POST /api/v1/auth/login +``` + +请求: + +```json +{ + "account": "admin", + "password": "******" +} +``` + +响应: + +```json +{ + "code": "200", + "message": "", + "data": { + "accessToken": "...", + "expiresIn": 7200, + "user": { + "id": "1", + "name": "admin" + } + } +} +``` + +建议: + +- 新系统使用 JWT Bearer。 +- 不再依赖“token 非空才校验”的旧逻辑。 +- 所有执行类接口默认要求授权。 + +### 5.2 新动作执行接口 + +```http +POST /api/v1/actions/{actionId}/execute +``` + +示例: + +```http +POST /api/v1/actions/quality.line.query/execute +``` + +请求: + +```json +{ + "dataSource": "mes-main", + "parameters": { + "lineCode": "L01", + "startTime": "2026-07-01 00:00:00", + "endTime": "2026-07-02 00:00:00" + }, + "page": { + "pageIndex": 1, + "pageSize": 50 + } +} +``` + +响应: + +```json +{ + "code": "200", + "message": "", + "data": { + "rows": [], + "total": 0 + } +} +``` + +说明: + +- `actionId` 是白名单动作,不是 SQL 文本。 +- `dataSource` 是已配置的数据源名。 +- `parameters` 只允许传动作定义中声明的参数。 + +### 5.3 旧协议兼容接口 + +```http +POST /api/v1/legacy/execute +``` + +请求兼容旧格式: + +```json +{ + "type": "11", + "name": "MES_工位_查询", + "param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]", + "token": "..." +} +``` + +实现策略: + +- 只作为迁移过渡接口。 +- 默认仍需 JWT。 +- 内部把 `type/name/param` 转换成 `LegacyCommandRequest`。 +- `name` 必须匹配白名单,不能任意执行。 +- 可通过配置逐步关闭高风险 Type,例如 `3/4/7/22/1001/1002/3001`。 + +### 5.4 文件上传接口 + +```http +POST /api/v1/files/{actionId}/upload +Content-Type: multipart/form-data +``` + +表单字段: + +- `metadata`:JSON 字符串,包含业务参数。 +- `file`:上传文件。 + +响应: + +```json +{ + "code": "200", + "message": "", + "data": { + "fileId": "123", + "fileName": "a.xlsx" + } +} +``` + +建议: + +- 不再用“最后三个参数必须是 filename/suffix/bytes”的隐式约定。 +- 在 action 定义中明确文件参数映射。 +- 限制文件大小、扩展名、MIME。 +- 大文件优先对象存储或文件系统,数据库只保存元数据;确需数据库保存时使用 BLOB/bytea/varbinary。 + +### 5.5 文件下载接口 + +```http +GET /api/v1/files/{actionId}/download?fileId=123 +``` + +响应: + +- 成功:`FileStreamResult` 或 `FileContentResult` +- 失败:统一 JSON 错误 + +文件响应头: + +```http +Content-Type: application/octet-stream +Content-Disposition: attachment; filename*=UTF-8''... +Access-Control-Expose-Headers: Content-Disposition +``` + +### 5.6 健康检查接口 + +```http +GET /health +GET /health/ready +GET /health/live +``` + +检查内容: + +- API 进程是否存活。 +- 数据源连接是否可用。 +- Redis/配置中心等依赖是否可用。 + +## 6. 数据库 Provider 抽象设计 + +### 6.1 核心接口 + +```csharp +public interface IDatabaseProvider +{ + DatabaseKind Kind { get; } + + Task QueryAsync(DbExecutionContext context, CancellationToken ct); + + Task ExecuteAsync(DbExecutionContext context, CancellationToken ct); + + Task ScalarAsync(DbExecutionContext context, CancellationToken ct); + + Task DownloadFileAsync(DbExecutionContext context, CancellationToken ct); +} +``` + +### 6.2 执行上下文 + +```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 Parameters { get; init; } = []; + public PageRequest? Page { get; init; } + public int CommandTimeoutSeconds { get; init; } = 60; +} +``` + +### 6.3 命令类型 + +```csharp +public enum CommandKind +{ + Text, + StoredProcedure, + Function, + FileDownload, + FileUpload +} +``` + +说明: + +- SQL Server:大量使用 `StoredProcedure`。 +- MySQL:可使用 `StoredProcedure` 或 `CALL proc(...)`。 +- PostgreSQL:查询型更建议封装为 `Function`,即 `select * from function(...)`;纯执行型可用 `procedure` 或 SQL。 + +### 6.4 Provider Factory + +```csharp +public sealed class DatabaseProviderFactory +{ + private readonly IReadOnlyDictionary _providers; + + public IDatabaseProvider Get(DatabaseKind kind) + { + if (_providers.TryGetValue(kind, out var provider)) + return provider; + + throw new NotSupportedException($"Unsupported database kind: {kind}"); + } +} +``` + +## 7. 多数据库差异处理 + +### 7.1 参数名称差异 + +| 数据库 | 常见参数形式 | 建议内部规范 | +| --- | --- | --- | +| SQL Server | `@ParamName` | 内部使用无前缀名,Provider 添加 `@` | +| MySQL | `@ParamName` 或 `?ParamName` | 内部使用无前缀名,Provider 统一转换 | +| PostgreSQL | `@ParamName`、`:ParamName` 或位置参数 | 内部使用无前缀名,Provider 统一转换 | + +内部 DTO: + +```json +{ + "name": "lineCode", + "value": "L01", + "type": "string", + "direction": "input" +} +``` + +不要让前台关心 `@`、`:`、`?`。 + +### 7.2 存储过程与函数差异 + +SQL Server: + +```sql +EXEC dbo.MES_Query @LineCode = @LineCode +``` + +PostgreSQL 推荐: + +```sql +select * from mes_query(@line_code); +``` + +MySQL: + +```sql +CALL mes_query(@lineCode); +``` + +迁移策略: + +- 不要求三种数据库共用同一段 SQL。 +- 对同一个 `actionId`,允许配置不同数据库的 `commandText`。 + +示例: + +```json +{ + "actionId": "quality.line.query", + "commands": { + "SqlServer": { + "kind": "StoredProcedure", + "text": "dbo.MES_Quality_LineQuery" + }, + "PostgreSql": { + "kind": "Function", + "text": "select * from mes_quality_line_query(@lineCode, @startTime, @endTime)" + }, + "MySql": { + "kind": "Text", + "text": "CALL mes_quality_line_query(@lineCode, @startTime, @endTime)" + } + } +} +``` + +### 7.3 分页差异 + +统一 API: + +```json +{ + "page": { + "pageIndex": 1, + "pageSize": 50 + } +} +``` + +Provider 翻译: + +| 数据库 | 分页写法 | +| --- | --- | +| SQL Server | `OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY` | +| PostgreSQL | `LIMIT @PageSize OFFSET @Offset` | +| MySQL | `LIMIT @Offset, @PageSize` 或 `LIMIT @PageSize OFFSET @Offset` | + +建议: + +- 新 SQL 动作尽量由 API 层统一追加分页。 +- 旧存储过程分页先兼容原有 `PageCurrent/PageSize/PageCount/ItemCount` 模式。 +- 对大表必须要求排序字段,禁止无序分页。 + +### 7.4 数据类型映射 + +| 逻辑类型 | SQL Server | PostgreSQL | MySQL | +| --- | --- | --- | --- | +| string | `nvarchar` | `text` / `varchar` | `varchar` / `text` | +| int | `int` | `integer` | `int` | +| long | `bigint` | `bigint` | `bigint` | +| decimal | `decimal(p,s)` | `numeric(p,s)` | `decimal(p,s)` | +| bool | `bit` | `boolean` | `tinyint(1)` / `boolean` | +| datetime | `datetime2` | `timestamp` / `timestamptz` | `datetime` | +| binary | `varbinary(max)` | `bytea` | `longblob` | +| json | `nvarchar(max)` / JSON functions | `jsonb` | `json` | + +建议内部参数类型: + +```text +string, int, long, decimal, bool, datetime, binary, json +``` + +### 7.5 标识符差异 + +| 数据库 | 标识符引用 | +| --- | --- | +| SQL Server | `[TableName]` | +| PostgreSQL | `"table_name"` | +| MySQL | `` `table_name` `` | + +要求: + +- 禁止前台直接传表名。 +- 如果必须动态表名,必须来自白名单。 +- Provider 负责引用标识符,不允许手工拼接未校验字符串。 + +## 8. ActionDefinition 白名单设计 + +### 8.1 为什么需要白名单 + +当前项目中 `Name/name` 可以是: + +- 存储过程名。 +- SQL 查询文本。 +- SQL 非查询文本。 +- 表名。 + +这在通用项目里风险很高。新系统必须把“可执行什么”变成后端配置,而不是前台决定。 + +### 8.2 白名单模型 + +```csharp +public sealed class ActionDefinition +{ + public string ActionId { get; init; } = ""; + public string DisplayName { get; init; } = ""; + public string Module { get; init; } = ""; + public bool Enabled { get; init; } + public string[] RequiredRoles { get; init; } = []; + public IReadOnlyList Parameters { get; init; } = []; + public IReadOnlyDictionary Commands { get; init; } + = new Dictionary(); +} +``` + +参数定义: + +```csharp +public sealed class ParameterDefinition +{ + public string Name { get; init; } = ""; + public string Type { get; init; } = "string"; + public bool Required { get; init; } + public int? MaxLength { get; init; } + public object? DefaultValue { get; init; } + public ParameterDirection Direction { get; init; } = ParameterDirection.Input; +} +``` + +命令定义: + +```csharp +public sealed class ProviderCommandDefinition +{ + public CommandKind Kind { get; init; } + public string Text { get; init; } = ""; + public bool ReturnsRows { get; init; } + public bool SupportsPaging { get; init; } +} +``` + +### 8.3 配置示例 + +```json +{ + "ActionId": "quality.line.query", + "DisplayName": "产线质量查询", + "Module": "Quality", + "Enabled": true, + "RequiredRoles": [ "quality.read" ], + "Parameters": [ + { "Name": "lineCode", "Type": "string", "Required": true, "MaxLength": 50 }, + { "Name": "startTime", "Type": "datetime", "Required": true }, + { "Name": "endTime", "Type": "datetime", "Required": true } + ], + "Commands": { + "SqlServer": { + "Kind": "StoredProcedure", + "Text": "dbo.MES_Quality_LineQuery", + "ReturnsRows": true, + "SupportsPaging": true + }, + "PostgreSql": { + "Kind": "Text", + "Text": "select * from mes_quality_line_query(@lineCode, @startTime, @endTime)", + "ReturnsRows": true, + "SupportsPaging": true + }, + "MySql": { + "Kind": "Text", + "Text": "CALL mes_quality_line_query(@lineCode, @startTime, @endTime)", + "ReturnsRows": true, + "SupportsPaging": false + } + } +} +``` + +配置可以先放 JSON 文件,后续迁移到数据库表。 + +## 9. 旧 Type 兼容设计 + +### 9.1 Type 映射表 + +| 旧 Type | 新项目处理 | +| --- | --- | +| `2001/2002/2003` | 迁移为报表导出 action | +| `2004/16` | 迁移为文件下载 action | +| `15` | 迁移为文件上传 action | +| `4000` | 独立 `/api/v1/client/ip` 或诊断接口 | +| `1/2/5` | 旧协议存储过程兼容,但必须白名单 | +| `11/111/12/13/21` | 新协议存储过程兼容,逐步改成 actionId | +| `1001/1002/3/4/7/22/3001` | 高风险,默认禁用或仅内网管理端启用 | +| `5001/5002/8888` | 迁移到 Auth/Password API,不继续放在通用 execute | + +### 9.2 兼容请求转换 + +旧请求: + +```json +{ + "type": "11", + "name": "MES_工位_查询", + "param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]" +} +``` + +转换为内部请求: + +```json +{ + "legacyType": 11, + "legacyName": "MES_工位_查询", + "actionId": "legacy.proc.MES_工位_查询", + "parameters": { + "工位号": "OP10" + } +} +``` + +关键规则: + +- `legacyName` 必须能匹配白名单。 +- 旧参数名中的 `@` 在内部去掉。 +- 旧类型 `Int/String/Boolean/DateTime` 映射为新类型。 +- 旧输出参数映射为 `ParameterDirection.Output`。 + +## 10. 统一响应格式 + +建议所有 JSON 响应统一: + +```json +{ + "code": "200", + "message": "", + "data": {}, + "traceId": "00-..." +} +``` + +分页: + +```json +{ + "code": "200", + "message": "", + "data": { + "rows": [], + "total": 0, + "pageIndex": 1, + "pageSize": 50 + }, + "traceId": "00-..." +} +``` + +错误: + +```json +{ + "code": "400", + "message": "参数 lineCode 必填", + "data": null, + "traceId": "00-..." +} +``` + +旧接口兼容: + +- `/api/v1/legacy/execute` 可提供 `compatibilityMode=true`,短期返回旧格式。 +- 默认建议返回新格式,并在 `data.legacyResult` 中保留旧结果。 + +## 11. 文件上传下载迁移方法 + +### 11.1 文件上传 + +当前旧逻辑: + +```text +Type=15 +读取第一个文件 +拆 name/suffix/bytes +把最后三个 SQL 参数覆盖为 filename/suffix/bytes +执行存储过程 +``` + +新逻辑: + +```text +POST /api/v1/files/{actionId}/upload + | + |-- 鉴权 + |-- 校验 actionId + |-- 校验文件大小/扩展名/MIME + |-- 校验 metadata 参数 + |-- 按 action 定义映射文件字段 + |-- Provider 执行数据库写入或对象存储写入 + |-- 返回统一 JSON +``` + +### 11.2 文件下载 + +当前旧逻辑: + +```text +Type=16/2004 +调用存储过程 +取 DataTable 第一行几列作为 fileName/suffix/bytes +BinaryWrite +``` + +新逻辑: + +```text +GET /api/v1/files/{actionId}/download + | + |-- 鉴权 + |-- 校验 actionId 和参数 + |-- Provider 查询文件元数据和内容 + |-- 返回 FileContentResult 或 FileStreamResult +``` + +不同数据库二进制字段: + +- SQL Server:`varbinary(max)` +- PostgreSQL:`bytea` +- MySQL:`longblob` + +建议: + +- 小文件可数据库存储。 +- 大文件使用对象存储或共享文件系统,数据库只保存路径、hash、大小、创建人、权限。 + +## 12. 鉴权、授权与审计 + +### 12.1 鉴权 + +推荐: + +- JWT Bearer。 +- 登录后签发 access token。 +- API 层统一 `[Authorize]`。 +- 健康检查和登录接口例外。 + +### 12.2 授权 + +按 action 授权: + +```text +用户角色 -> 权限码 -> ActionDefinition.RequiredRoles +``` + +示例: + +```json +{ + "actionId": "quality.line.query", + "requiredRoles": [ "quality.read" ] +} +``` + +### 12.3 审计日志 + +每次执行记录: + +- `traceId` +- 用户 ID +- 来源 IP +- actionId +- dataSource +- databaseKind +- commandKind +- 参数摘要,不记录敏感原文 +- 执行耗时 +- 返回行数 +- 是否成功 +- 错误码和错误摘要 + +审计日志建议写入独立表或日志平台。 + +## 13. 配置设计 + +### 13.1 appsettings 示例 + +```json +{ + "Database": { + "DefaultDataSource": "mes-main", + "DataSources": { + "mes-main": { + "Kind": "SqlServer", + "ConnectionStringName": "MES_MAIN" + }, + "mes-pg": { + "Kind": "PostgreSql", + "ConnectionStringName": "MES_PG" + }, + "mes-mysql": { + "Kind": "MySql", + "ConnectionStringName": "MES_MYSQL" + } + } + }, + "Security": { + "JwtIssuer": "MesUniversalApi", + "JwtAudience": "MesFrontend", + "AccessTokenMinutes": 120 + }, + "Cors": { + "AllowedOrigins": [ "https://mes.example.com" ] + } +} +``` + +连接字符串不建议写入仓库,推荐: + +- 环境变量。 +- Docker secret。 +- Kubernetes secret。 +- 企业密钥管理服务。 + +### 13.2 Linux 环境变量示例 + +```bash +export ConnectionStrings__MES_MAIN='Server=...;Database=...;User Id=...;Password=...;TrustServerCertificate=True' +export ConnectionStrings__MES_PG='Host=...;Database=...;Username=...;Password=...' +export ConnectionStrings__MES_MYSQL='Server=...;Database=...;User ID=...;Password=...' +``` + +## 14. Linux 部署路线 + +### 14.1 Docker 部署 + +推荐 Dockerfile: + +```dockerfile +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime +WORKDIR /app +COPY publish/ . +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 +ENTRYPOINT ["dotnet", "MesUniversalApi.Api.dll"] +``` + +部署: + +```bash +dotnet publish -c Release -o publish +docker build -t mes-universal-api:1.0.0 . +docker run -d --name mes-api -p 8080:8080 --env-file .env mes-universal-api:1.0.0 +``` + +### 14.2 systemd 部署 + +```ini +[Unit] +Description=MES Universal API +After=network.target + +[Service] +WorkingDirectory=/opt/mes-api +ExecStart=/usr/bin/dotnet /opt/mes-api/MesUniversalApi.Api.dll +Restart=always +RestartSec=5 +Environment=ASPNETCORE_URLS=http://0.0.0.0:8080 +Environment=ASPNETCORE_ENVIRONMENT=Production + +[Install] +WantedBy=multi-user.target +``` + +### 14.3 Nginx 反向代理 + +```nginx +server { + listen 80; + server_name mes-api.example.com; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +## 15. 分阶段实施计划 + +### 阶段 0:资产盘点 + +目标: + +- 列出当前所有前台调用的 `Type/Name/Param`。 +- 统计文件上传下载接口。 +- 统计直接 SQL 类 Type。 +- 统计存储过程、表、视图、函数依赖。 + +输出: + +- `legacy_api_inventory.xlsx` +- `stored_procedure_inventory.xlsx` +- `sql_risk_list.md` + +### 阶段 1:WebAPI 基础框架 + +目标: + +- 建立 ASP.NET Core WebAPI 项目。 +- 配置 JWT、Swagger/OpenAPI、CORS、HealthCheck、日志。 +- 建立统一响应结构。 +- 建立 `IDatabaseProvider` 抽象。 + +验收: + +- Linux 上能启动。 +- `/health` 正常。 +- Swagger 可访问。 +- 三种数据库至少能完成连接测试。 + +### 阶段 2:SQL Server 兼容优先 + +目标: + +- 先支持当前 SQL Server 业务。 +- 实现 `LegacyController`,兼容 `Type=11/12/13/15/16/2004`。 +- 实现文件上传下载。 +- 建立白名单。 + +策略: + +- 先不迁移数据库,只把入口从 `ashx` 换成 WebAPI。 +- 前台可逐步切换地址。 + +验收: + +- 选 10 个高频接口完成回归。 +- 返回数据与旧接口一致或可映射。 +- 文件上传下载可用。 + +### 阶段 3:PostgreSQL Provider + +目标: + +- 实现 PostgreSQL 连接、查询、执行、文件二进制。 +- 将选定业务动作迁移到 PostgreSQL function 或 SQL。 + +重点: + +- 存储过程返回结果集建议改 PostgreSQL function。 +- 输出参数尽量改为结果列或 JSON 返回。 +- 分页改为 `LIMIT/OFFSET`。 + +验收: + +- 同一个 `actionId` 可在 SQL Server 和 PostgreSQL 上运行。 +- 对比测试结果一致。 + +### 阶段 4:MySQL Provider + +目标: + +- 实现 MySQL 连接、查询、执行、文件二进制。 +- 将选定业务动作迁移到 MySQL procedure 或 SQL。 + +重点: + +- MySQL 存储过程和输出参数语义与 SQL Server 不同。 +- 大量动态 SQL 应改为 provider-specific SQL 模板。 +- 分页使用 `LIMIT/OFFSET`。 + +验收: + +- 同一个 `actionId` 可在 SQL Server、PostgreSQL、MySQL 上分别运行。 + +### 阶段 5:安全治理与旧接口退场 + +目标: + +- 禁用高风险旧 Type。 +- 前台从 `legacy/execute` 迁移到 `actions/{actionId}/execute`。 +- 完成审计、限流、告警。 + +验收: + +- 不再允许前台传任意 SQL。 +- 所有执行类接口都有 action 白名单和授权。 +- 审计日志可追踪用户、动作、耗时和结果。 + +## 16. 测试方法 + +### 16.1 单元测试 + +覆盖: + +- 旧 `Param` 解析。 +- 新 `Param` JSON 数组解析。 +- 类型转换。 +- 输出参数映射。 +- action 白名单校验。 +- 响应格式封装。 + +### 16.2 集成测试 + +使用 Docker Compose 启动: + +- SQL Server +- PostgreSQL +- MySQL +- API + +对同一 action 执行: + +- 查询。 +- 增删改。 +- 分页。 +- 文件上传。 +- 文件下载。 +- token 成功和失败。 + +### 16.3 回归测试 + +从旧系统采样请求: + +```text +旧请求 -> 旧 ashx 响应 +旧请求 -> 新 legacy/execute 响应 +对比字段、行数、文件 hash、错误码 +``` + +### 16.4 性能测试 + +重点: + +- 大结果集查询。 +- 分页查询。 +- 大文件上传下载。 +- 长事务或长存储过程。 +- 数据库连接池。 + +工具: + +- k6 +- JMeter +- dotnet-counters +- OpenTelemetry + Prometheus/Grafana + +## 17. 关键实现方法 + +### 17.1 请求标准化 + +```text +HTTP 请求 + | + |-- 新接口:actionId + parameters + |-- 旧接口:type/name/param + | + v +RequestNormalizer + | + |-- 参数名去前缀 @/:/? + |-- 类型转换 + |-- 默认值填充 + |-- 必填校验 + |-- 白名单校验 + | + v +DbExecutionContext +``` + +### 17.2 Provider 执行 + +```text +ActionService + | + |-- 根据 dataSource 找 DatabaseKind + |-- 根据 actionId 找 ActionDefinition + |-- 根据 DatabaseKind 选 command + |-- DatabaseProviderFactory.Get(kind) + |-- provider.QueryAsync / ExecuteAsync + |-- 统一封装 ApiResponse +``` + +### 17.3 动态结果序列化 + +建议: + +- 小结果集可先读入 `List>`。 +- 大结果集使用 `DbDataReader` + `Utf8JsonWriter` 流式写出。 +- 对日期、decimal、byte[] 做统一序列化策略。 + +### 17.4 错误处理 + +统一中间件: + +```text +ExceptionHandlingMiddleware + | + |-- 捕获异常 + |-- 记录 traceId、userId、actionId + |-- 返回统一 JSON +``` + +错误码建议: + +| code | 含义 | +| --- | --- | +| `200` | 成功 | +| `400` | 参数错误 | +| `401` | 未登录 | +| `403` | 无权限 | +| `404` | action 不存在或未启用 | +| `409` | 业务冲突 | +| `500` | 系统错误 | +| `DB001` | 数据库连接失败 | +| `DB002` | SQL/存储过程执行失败 | +| `FILE001` | 文件不存在 | +| `FILE002` | 文件大小或类型不允许 | + +## 18. 数据库迁移策略 + +### 18.1 不建议自动翻译所有 SQL + +SQL Server 到 PostgreSQL/MySQL 不能只靠字符串替换,差异包括: + +- 存储过程语法。 +- 临时表。 +- `TOP`、`OFFSET`、分页。 +- `ISNULL`、`GETDATE`、字符串函数。 +- `IDENTITY`、序列、自增。 +- `nvarchar`、`bit`、`datetime2`、`varbinary(max)`。 +- 事务和锁语义。 + +### 18.2 推荐按 action 迁移 + +迁移粒度: + +```text +一个 actionId = 一个可测试的业务能力 +``` + +迁移步骤: + +1. 固化旧 SQL Server 行为。 +2. 编写 PostgreSQL/MySQL 版本 SQL 或函数。 +3. 用相同测试输入比较输出。 +4. 通过后把该 action 标记为多数据库支持。 + +### 18.3 高风险 Type 处置 + +| Type | 风险 | 建议 | +| --- | --- | --- | +| `3` | 直接 SQL 查询 | 仅内网调试或禁用 | +| `4` | 直接 SQL 非查询 | 禁用 | +| `7` | 动态 DROP/CREATE 表 | 改为固定导入表或临时表 | +| `22` | 直接 SQL 返回新结构 | 改为 actionId | +| `1001/1002` | 客户端传 SQL | 改为 SQL 模板白名单 | +| `3001` | SQL 命令执行入口 | 禁用或仅管理员审计使用 | + +## 19. 交付物建议 + +第一批交付物: + +- 新 WebAPI 项目骨架。 +- `IDatabaseProvider` 抽象。 +- SQL Server Provider。 +- PostgreSQL Provider。 +- MySQL Provider。 +- Legacy Type 11/12/13/15/16/2004 兼容。 +- action 白名单配置。 +- JWT 鉴权。 +- 统一响应和异常处理。 +- Dockerfile 和 Linux 部署文档。 +- 集成测试环境。 + +第二批交付物: + +- 报表导出接口。 +- 文件存储重构。 +- 高风险 Type 替代方案。 +- 审计日志和后台查询。 +- 前台 API SDK 或 TypeScript client。 + +## 20. 参考资料 + +- Microsoft .NET 支持策略: +- EF Core 数据库 Provider 列表: +- Npgsql PostgreSQL .NET Provider: +- Npgsql EF Core Provider: +- MySqlConnector 文档: +- MySQL Connector/NET 官方文档: + +## 21. 总结 + +本项目不建议简单把 `MESCommonBase.ashx` 改写成一个新的“万能 SQL WebAPI”。正确路线是: + +```text +旧 Type 协议兼容 + -> actionId 白名单 + -> Provider 策略层 + -> 统一鉴权和响应 + -> SQL Server 先落地 + -> PostgreSQL/MySQL 按 action 逐步迁移 + -> 关闭高风险动态 SQL 能力 +``` + +这样既能保持现有业务可迁移,又能支撑 Linux WebAPI 和多数据库目标,并为后续安全治理、审计、前台接口稳定性打基础。 diff --git a/working/build_mescommonbase_migration_ppt.py b/working/build_mescommonbase_migration_ppt.py new file mode 100644 index 0000000..46fce4a --- /dev/null +++ b/working/build_mescommonbase_migration_ppt.py @@ -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", "旧协议 DTO,Type/Name/Param/token 等字段"], + ["WriteExcelNPOI.dll / ExcelWebCall", "2001/2002/2003 Excel/PDF 导出"], + ["Newtonsoft.Json、NPOI、Spire、Lazy.Captcha 等", "bin 目录运行依赖,版本需一致或验证兼容"], + ["Web.config appSettings ConnectionString", "默认连接串来源,当前使用明文配置"], + ] + deck.table(slide, 0.65, 1.35, 12.05, 4.95, data, col_widths=[4.0, 8.05], font_size=9.1) + deck.add_footer(slide) + + # 6 section + deck.section_slide("第一部分:主流程与响应输出", "说明 MESCommonBase 如何从 type 分支走向二进制下载、JSON 文本、IP 文本和异常兜底。") + + # 7 full overview + slide = deck.blank() + deck.title(slide, "MESCommonBase 总体流程图", "完整图用于把入口解析、Type 分支、响应出口和风险位置放在一张图中查看。") + deck.add_image_fit(slide, "总流程_完整.png", 0.55, 1.15, 12.25, 5.85) + deck.add_footer(slide) + + # 8 entry crop + slide = deck.blank() + deck.title(slide, "入口解析与 Type 识别", "迁移时要保持三种入参来源的兼容:JSON body、jsonobj、Request[\"param\"]。") + deck.add_image_fit(slide, "总流程_入口解析.png", 0.65, 1.2, 7.0, 4.9) + deck.bullets( + slide, + 8.0, + 1.45, + 4.55, + 4.6, + [ + "默认 ContentType 设置为 application/json。", + "先用 JsonMapper 解析,再尝试反序列化 jsonobj。", + "multipart/form-data 场景通常依赖 Request[\"param\"]。", + "解析失败被吞掉,type 可能保持默认 0 并进入 default。", + ], + font_size=13.2, + title="入口兼容点", + ) + deck.add_footer(slide) + + # 9 type branch crop + slide = deck.blank() + deck.title(slide, "Type 首层分支总览", "首层分支决定响应出口:文件流、上传 JSON、IP 文本或进入 SqlWebCall。") + deck.add_image_fit(slide, "总流程_Type分支.png", 0.55, 1.18, 12.2, 4.05) + data = [ + ["Type", "方向", "输出"], + ["2001/2002/2003/2004/16", "文件生成或数据库文件读取", "二进制下载"], + ["15", "文件上传并调用存储过程", "JSON 文本"], + ["4000", "获取请求 IP", "文本 IP"], + ["default", "DataLink.SqlWebCall 二次分发", "JSON 或空字符串"], + ] + deck.table(slide, 1.0, 5.45, 11.3, 1.25, data, col_widths=[2.7, 5.2, 3.4], font_size=8.5) + deck.add_footer(slide) + + # 10 response main + slide = deck.blank() + deck.title(slide, "响应输出主流程图", "主图将四类响应出口分开:文件下载、上传 JSON、IP 文本、default JSON。") + deck.add_image_fit(slide, "响应主_分支出口.png", 0.55, 1.15, 12.25, 5.75) + deck.add_footer(slide) + + # 11 response rules + slide = deck.blank() + deck.title(slide, "响应规则与异常兜底", "迁移时要特别关注 Response.End、空响应和异常不记录的问题。") + deck.add_image_fit(slide, "响应主_规则异常.png", 0.7, 1.22, 11.9, 3.2) + deck.bullets( + slide, + 1.0, + 4.75, + 11.2, + 1.55, + [ + "文件下载统一设置 application/octet-stream 和 Content-Disposition,前端通过响应头取文件名。", + "外层 catch 只写 responseText,默认值为 NULL,不设置 HTTP 状态码。", + "Type=16 若 bytes 为 null 直接 return,调用方可能收到空响应。", + ], + font_size=13.1, + ) + deck.add_footer(slide) + + # 12 section + deck.section_slide("第二部分:SqlWebCall 默认分支", "说明普通业务请求进入 DataLink.SqlWebCall 后如何初始化、二次分发、解析参数、执行数据库并返回。") + + # 13 secondary full + slide = deck.blank() + deck.title(slide, "SqlWebCall 默认分支完整图", "完整图用于快速定位初始化、四类 Type、数据库执行和返回出口。") + deck.add_image_fit(slide, "SqlWebCall_完整.png", 0.55, 1.1, 12.25, 5.9) + deck.add_footer(slide) + + # 14 init + slide = deck.blank() + deck.title(slide, "SqlWebCall 初始化流程", "第一次进入时读取 Web.config 的 ConnectionString,并通过静态标记复用。") + deck.add_image_fit(slide, "SqlWebCall_初始化.png", 0.75, 1.25, 7.0, 3.05) + deck.bullets( + slide, + 8.1, + 1.35, + 4.45, + 3.7, + [ + "InitSystemReg 当前直接读取 appSettings[\"ConnectionString\"]。", + "原注册号、序列号、加密连接串校验逻辑已注释。", + "移植后需确保 Web.config 中连接串名称和权限可用。", + ], + font_size=13, + title="迁移关注点", + ) + deck.card(slide, 0.95, 5.35, 11.35, 0.75, "结论", "数据库连接配置是 SqlWebCall 能否工作的第一检查点;同时建议在移植阶段评估明文连接串和账号权限。", "red_light", "red", "red") + deck.add_footer(slide) + + # 15 type secondary + slide = deck.blank() + deck.title(slide, "SqlWebCall Type 二次分发", "default 分支内部按 Type 再拆成四类:登录/加密、旧协议、新协议、SQL/命令。") + deck.add_image_fit(slide, "SqlWebCall_Type二次分发.png", 0.55, 1.12, 12.25, 5.9) + deck.add_footer(slide) + + # 16 type table + slide = deck.blank() + deck.title(slide, "SqlWebCall Type 能力表", "迁移测试时可按这些 Type 分组准备用例。") + data = [ + ["分类", "Type", "方法/能力", "迁移重点"], + ["登录/加密", "8888/5001/5002", "加密、注册/改密、登录生成 token", "确认加密逻辑和 token 存储过程"], + ["旧协议存储过程", "1/2/5", "jsonobj + 拼接字符串 Param", "兼容旧前端参数格式"], + ["新协议存储过程", "11/111/12/13/21", "JsonData + JSON 数组字符串 Param", "确认存储过程名、参数、分页输出"], + ["SQL/命令", "1001/1002/3/4/7/22/3001", "直接 SQL、建表导入、命令执行", "强烈建议白名单和权限隔离"], + ] + deck.table(slide, 0.55, 1.3, 12.25, 4.3, data, col_widths=[2.0, 2.3, 4.2, 3.75], font_size=8.2) + deck.card(slide, 0.75, 6.0, 11.85, 0.65, "关键判断", "这部分是移植风险最大的区域:协议兼容、数据库权限、SQL 执行能力、token 逻辑都集中在 SqlWebCall 下游。", "amber_light", "amber", "amber") + deck.add_footer(slide) + + # 17 param protocol + slide = deck.blank() + deck.title(slide, "参数协议:新协议与旧协议", "迁移时要避免把 Param 当作普通对象,它在新协议中通常是 JSON 数组字符串。") + deck.card(slide, 0.7, 1.25, 5.9, 4.7, "新协议 Param", "适用:11/111/12/13/15/16/21/2004 等\n\n格式:\n[{\"name\":\"@p\",\"value\":\"v\",\"type\":\"string\"}]\n\n特点:\n- Param 字段本身是字符串\n- 支持 type 和 output\n- 数组 value 会转逗号字符串", "green_light", "green", "green") + deck.card(slide, 6.95, 1.25, 5.65, 4.7, "旧协议 Param", "适用:1/2/5 等\n\nType 1/2:\n@p=value=type&@out=0=int=output\n\nType 5:\n@p&value&type|@p2&value&type\n\n特点:\n- 分隔符敏感\n- 值包含 &、=、| 时容易出错", "purple_light", "purple", "purple") + deck.add_footer(slide) + + # 18 db and return + slide = deck.blank() + deck.title(slide, "数据库执行链路与返回格式", "SqlWebCall 只路由,真正执行在 DataLinkMesWork.cs 和 SQLCommon.cs。") + deck.add_image_fit(slide, "SqlWebCall_数据库返回.png", 0.62, 1.15, 12.05, 4.0) + deck.bullets( + slide, + 0.85, + 5.45, + 11.8, + 1.1, + [ + "存储过程使用 ExecuteStoredProcedure,CommandType=StoredProcedure,CommandTimeout=0。", + "SQL 查询使用 ExecuteSelectMesWork / ExecuteDataTable / ExecuteDataset。", + "返回格式混合:DataTable JSON、result=1/0、rows-total、code-message-data、空字符串。", + ], + font_size=12.6, + ) + deck.add_footer(slide) + + # 19 risk + slide = deck.blank() + deck.title(slide, "风险与迁移控制点", "这些问题不一定阻塞移植,但会影响上线后的安全性和可维护性。") + left = [ + "token 多数是非空才校验;不传 token 的路径常见不拒绝。", + "客户端可控制 Name/name,可能是存储过程名、SQL 文本或表名。", + "Type=7 可按请求内容 DROP/CREATE 表。", + "CommandTimeout=0,慢 SQL 或阻塞存储过程可能长期占用请求。", + ] + right = [ + "下载异常和 bytes=null 缺少统一错误响应。", + "外层 catch 不记录日志,排障成本高。", + "明文连接串和数据库账号权限需要重新评估。", + "响应格式不统一,前端和联调工具需按 Type 分别判断。", + ] + deck.card(slide, 0.75, 1.28, 5.85, 4.9, "SqlWebCall 风险", "\n".join(f"• {x}" for x in left), "red_light", "red", "red") + deck.card(slide, 6.95, 1.28, 5.65, 4.9, "响应与部署风险", "\n".join(f"• {x}" for x in right), "amber_light", "amber", "amber") + deck.add_footer(slide) + + # 20 section + deck.section_slide("第三部分:项目移植资料", "给迁移实施人员使用:环境、配置、文件、数据库、接口验证和上线前检查。") + + # 21 migration checklist + slide = deck.blank() + deck.title(slide, "移植检查清单", "按部署前必须确认的对象拆分。") + data = [ + ["类别", "检查项", "说明"], + ["运行环境", ".NET Framework 4.8 / IIS / ASP.NET", "WebHandler 运行环境需一致或验证兼容"], + ["Web 文件", "MES_Manage/submit/MESCommonBase.ashx", "路径和 Class 名需保持"], + ["DLL 依赖", "Bin 下 DataLinkMesWork、BasicData、WriteExcelNPOI 等", "版本差异会影响运行和导出"], + ["配置", "Web.config / ConnectionString / CORS", "连接串、跨域、默认文档、handlers"], + ["数据库", "存储过程、表、权限、token 过程", "SqlWebCall 能力依赖数据库对象"], + ["文件导出", "NPOI、Spire、模板逻辑", "2001/2002/2003 验证"], + ["安全", "token、白名单、日志、账号权限", "建议迁移阶段同步补强"], + ] + deck.table(slide, 0.55, 1.28, 12.25, 5.25, data, col_widths=[1.5, 4.4, 6.35], font_size=8.5) + deck.add_footer(slide) + + # 22 migration steps + slide = deck.blank() + deck.title(slide, "建议移植实施顺序", "先让原协议跑通,再逐步补齐安全和可维护性。") + steps = [ + ("1", "环境准备", ".NET/IIS/站点路径/应用程序池"), + ("2", "文件迁移", "ashx、Bin、Web.config、模板和静态资源"), + ("3", "数据库迁移", "库表、存储过程、权限、token 相关过程"), + ("4", "接口冒烟", "15/16/2001/2002/4000/default 常用 Type"), + ("5", "风险补强", "鉴权、白名单、日志、连接串和权限"), + ("6", "回归验收", "按 Type 分组准备测试矩阵和下载校验"), + ] + for i, (num, title, body) in enumerate(steps): + x = 0.75 + (i % 3) * 4.05 + y = 1.35 + (i // 3) * 2.35 + deck.card(slide, x, y, 3.55, 1.65, f"{num}. {title}", body, "white", "blue" if i < 3 else "green", "navy") + deck.card(slide, 1.1, 6.3, 11.1, 0.55, "执行原则", "先保持协议兼容,再处理风险收敛;不要在未完成回归测试前重构 Param 协议或响应格式。", "amber_light", "amber", "amber") + deck.add_footer(slide) + + # 23 test matrix + slide = deck.blank() + deck.title(slide, "迁移验证用例矩阵", "每组至少准备一个成功、一个失败、一个边界场景。") + data = [ + ["场景", "覆盖 Type", "验证点"], + ["Excel/PDF 导出", "2001/2002/2003", "文件名、扩展名、下载流、跨域响应头"], + ["数据库文件下载", "16/2004", "bytes 为空、文件名、后缀、二进制一致性"], + ["文件上传", "15", "无文件、有文件、文件名无扩展名、大文件"], + ["IP 查询", "4000", "代理头、REMOTE_ADDR、UserHostAddress"], + ["旧协议", "1/2/5", "Param 分隔符、输出参数、分页、token"], + ["新协议", "11/111/12/13/21", "JSON 数组 Param、分页、DataSet、新结构返回"], + ["SQL 执行", "1001/1002/3/4/7/22/3001", "SQL 参数、权限、白名单、错误处理"], + ] + deck.table(slide, 0.55, 1.25, 12.25, 5.45, data, col_widths=[2.35, 2.5, 7.4], font_size=8.3) + deck.add_footer(slide) + + # 24 recommended improvements + slide = deck.blank() + deck.title(slide, "移植后的改造建议", "不建议在第一天全部重构,但建议列入上线前后计划。") + deck.card(slide, 0.75, 1.25, 3.75, 4.95, "短期", "• 统一 token 鉴权\n• 未支持 Type 返回明确错误\n• 下载空文件返回可识别错误\n• 外层 catch 写日志\n• 修正 Type=4000 IP 逻辑", "green_light", "green", "green") + deck.card(slide, 4.8, 1.25, 3.75, 4.95, "中期", "• 建立 Type + Name 白名单\n• Param 从字符串迁移为 JSON 数组\n• 统一 JSON 返回结构\n• SQL 执行能力与普通接口分离", "blue_light", "blue", "blue") + deck.card(slide, 8.85, 1.25, 3.75, 4.95, "长期", "• 按业务模块拆分接口\n• 数据库账号最小权限\n• 移除明文连接串\n• 加入审计日志和限流", "purple_light", "purple", "purple") + deck.add_footer(slide) + + # 25 appendix + slide = deck.blank() + deck.title(slide, "附录:资料追溯与后续维护", "PPT 是汇报和迁移视图,Markdown 与 Mermaid 是详细资料和可维护源。") + deck.bullets( + slide, + 0.9, + 1.35, + 11.9, + 4.6, + [ + "详细文字资料:MESCommonBase程序梳理.md、MESCommonBase响应输出与SqlWebCall默认分支详细文档.md。", + "流程图源文件:MESCommonBase流程图.mmd、MESCommonBase响应输出与SqlWebCall主流程图.mmd、MESCommonBase响应输出与SqlWebCall次流程图.mmd。", + "高清流程图:三张 PNG 已在 working 中保存,PPT 中按完整图和局部截图引用。", + "自动生成脚本:render_*.py 与 build_mescommonbase_migration_ppt.py,可在文档更新后重新生成资料。", + ], + font_size=14, + ) + deck.card(slide, 1.1, 6.1, 11.1, 0.65, "输出文件", "working/MESCommonBase迁移分析资料包.pptx", "white", "blue", "blue") + deck.add_footer(slide) + + deck.save() + + +if __name__ == "__main__": + build_deck() + print(OUT) diff --git a/working/ppt_assets/SqlWebCall_Type二次分发.png b/working/ppt_assets/SqlWebCall_Type二次分发.png new file mode 100644 index 0000000..44491b2 Binary files /dev/null and b/working/ppt_assets/SqlWebCall_Type二次分发.png differ diff --git a/working/ppt_assets/SqlWebCall_初始化.png b/working/ppt_assets/SqlWebCall_初始化.png new file mode 100644 index 0000000..e189774 Binary files /dev/null and b/working/ppt_assets/SqlWebCall_初始化.png differ diff --git a/working/ppt_assets/SqlWebCall_完整.png b/working/ppt_assets/SqlWebCall_完整.png new file mode 100644 index 0000000..fcf5404 Binary files /dev/null and b/working/ppt_assets/SqlWebCall_完整.png differ diff --git a/working/ppt_assets/SqlWebCall_数据库返回.png b/working/ppt_assets/SqlWebCall_数据库返回.png new file mode 100644 index 0000000..f3f8a73 Binary files /dev/null and b/working/ppt_assets/SqlWebCall_数据库返回.png differ diff --git a/working/ppt_assets/SqlWebCall_风险.png b/working/ppt_assets/SqlWebCall_风险.png new file mode 100644 index 0000000..ef1c82f Binary files /dev/null and b/working/ppt_assets/SqlWebCall_风险.png differ diff --git a/working/ppt_assets/响应主_分支出口.png b/working/ppt_assets/响应主_分支出口.png new file mode 100644 index 0000000..ec0c580 Binary files /dev/null and b/working/ppt_assets/响应主_分支出口.png differ diff --git a/working/ppt_assets/响应主_完整.png b/working/ppt_assets/响应主_完整.png new file mode 100644 index 0000000..a2265a8 Binary files /dev/null and b/working/ppt_assets/响应主_完整.png differ diff --git a/working/ppt_assets/响应主_规则异常.png b/working/ppt_assets/响应主_规则异常.png new file mode 100644 index 0000000..65bb35a Binary files /dev/null and b/working/ppt_assets/响应主_规则异常.png differ diff --git a/working/ppt_assets/总流程_Type分支.png b/working/ppt_assets/总流程_Type分支.png new file mode 100644 index 0000000..c262f24 Binary files /dev/null and b/working/ppt_assets/总流程_Type分支.png differ diff --git a/working/ppt_assets/总流程_入口解析.png b/working/ppt_assets/总流程_入口解析.png new file mode 100644 index 0000000..d8b0cc5 Binary files /dev/null and b/working/ppt_assets/总流程_入口解析.png differ diff --git a/working/ppt_assets/总流程_响应默认.png b/working/ppt_assets/总流程_响应默认.png new file mode 100644 index 0000000..cc9ae29 Binary files /dev/null and b/working/ppt_assets/总流程_响应默认.png differ diff --git a/working/ppt_assets/总流程_完整.png b/working/ppt_assets/总流程_完整.png new file mode 100644 index 0000000..aada159 Binary files /dev/null and b/working/ppt_assets/总流程_完整.png differ diff --git a/working/render_mescommonbase_flowchart.py b/working/render_mescommonbase_flowchart.py new file mode 100644 index 0000000..7912b18 --- /dev/null +++ b/working/render_mescommonbase_flowchart.py @@ -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(stream)\n成功后用 dataobj.Type 覆盖 type", "#EAF7EF", "#227A45", font=font_box) + draw_diamond(cx, 1090, 520, 190, "dataobj\n== null?", "#FFF4CC", "#B27700", font=font_box) + draw_box(3890, 1090, 840, 130, '读取 Request["param"]\n再次解析 Type / type', "#FFF7E6", "#B27700", font=font_box_small) + draw_diamond(cx, 1315, 500, 180, "switch\n(type)", "#FFF4CC", "#B27700", font=font_box) + + arrow([(cx, 348), (cx, 402)]) + arrow([(cx, 548), (cx, 598)]) + arrow([(cx, 742), (cx, 792)]) + arrow([(cx, 938), (cx, 995)]) + arrow([(cx + 260, 1090), (3470, 1090)], label="是", label_pos=(3140, 1050)) + arrow([(3890, 1155), (3890, 1240), (2850, 1240), (2850, 1315)]) + arrow([(cx, 1185), (cx, 1225)], label="否", label_pos=(2555, 1210)) + + +def draw_branch_panel(): + panel_x, panel_y, panel_w, panel_h = 160, 1460, 4880, 1120 + draw_panel(panel_x, panel_y, panel_w, panel_h, "Type 分发分支", fill="#FFFFFF", outline="#C7D2E5") + arrow([(W / 2, 1405), (W / 2, panel_y + 10)]) + + card_w, card_h = 1080, 330 + xs = [790, 1960, 3130, 4300] + y1, y2 = 1740, 2205 + cards = [ + (xs[0], y1, "2001 Excel 导出\nExcelWebCall.ExcelFile(jsonData)\n生成 bytes / fileName / extension\n响应:二进制下载", "#EAF7EF", "#227A45"), + (xs[1], y1, "2002 PDF 导出\nExcelWebCall.ExcelFilePdf(jsonData)\nExcel 转 PDF\n响应:二进制下载", "#EAF7EF", "#227A45"), + (xs[2], y1, "2003 合成 Excel 图片/扩展名导出\n读取 Form 第一项 dataimg[0]\nExcelWebCall.ExcelFile(jsonData)\n响应:二进制下载", "#EAF7EF", "#227A45"), + (xs[3], y1, "2004 数据库文件下载\nDataLink.ExePROCEDURE_Type2004\n返回 bytes / fileName / extension\n响应:二进制下载", "#EAF7EF", "#227A45"), + (xs[0], y2, "15 文件上传\n读取 Request.Files[0]\n拆分 name / suffix / bytes\nDataLink.ExePROCEDURE_Type15\n响应:JSON", "#EEF2FF", "#4F46E5"), + (xs[1], y2, "16 数据库文件下载\nDataLink.ExePROCEDURE_Type16\n返回 fileName / suffix / bytes\nbytes 为 null 时直接 return", "#EAF7EF", "#227A45"), + (xs[2], y2, "4000 IP 查询\n读取 ServerVariables / UserHostAddress\n响应:文本 IP", "#FFF7E6", "#B27700"), + (xs[3], y2, "default 通用业务调用\nDataLink.SqlWebCall(type,jsonData,dataobj)\n进入数据库/存储过程二次分发", "#F3E8FF", "#7E22CE"), + ] + for cx, cy, text, fill, outline in cards: + draw_box(cx, cy, card_w, card_h, text, fill, outline, font=font_box_small, radius=28) + + return panel_x, panel_y, panel_w, panel_h, xs, y2, card_h + + +def draw_detail_panels(panel_info): + panel_x, panel_y, panel_w, panel_h, xs, y2, card_h = panel_info + resp_x, resp_y, resp_w, resp_h = 260, 2740, 2050, 740 + detail_x, detail_y, detail_w, detail_h = 2520, 2740, 2420, 740 + + draw_panel(resp_x, resp_y, resp_w, resp_h, "响应输出", fill="#FFFFFF", outline="#C7D2E5") + resp_text = ( + "文件分支 2001/2002/2003/2004/16:\n" + " application/octet-stream + Content-Disposition\n" + " BinaryWrite(bytes) + Flush/End\n\n" + "Type 15 与 default:\n" + " Response.Write(JSON 文本)\n\n" + "Type 4000:\n" + " Response.Write(userIP)\n\n" + "外层 catch:异常时写当前 responseText,初始值为 NULL" + ) + draw_multiline_left(resp_text, resp_x + 60, resp_y + 115, resp_w - 120, font_note, fill="#344054", line_gap=12) + + draw_panel(detail_x, detail_y, detail_w, detail_h, "DataLink.SqlWebCall 默认分支", fill="#FFFFFF", outline="#C7D2E5") + detail_text = ( + "初始化:InitSystemReg 读取 Web.config 的 ConnectionString\n\n" + "8888 / 5001 / 5002:加密、注册/改密、登录并生成 token\n\n" + "1 / 2 / 5:旧协议存储过程,Param 为拼接字符串\n\n" + "11 / 111 / 12 / 13 / 21:新协议存储过程,Param 为 JSON 数组字符串\n\n" + "1001 / 1002 / 3 / 4 / 7 / 22 / 3001:直接 SQL、建表导入或 SQL 命令执行\n\n" + "输出:数组 JSON 或 { code, message, data }" + ) + draw_multiline_left(detail_text, detail_x + 60, detail_y + 115, detail_w - 120, font_note, fill="#344054", line_gap=12) + + arrow([(xs[3], y2 + card_h / 2), (xs[3], 2630), (detail_x + detail_w / 2, 2630), (detail_x + detail_w / 2, detail_y)], color="#7E22CE") + arrow([(panel_x + 1150, panel_y + panel_h), (panel_x + 1150, resp_y)], color="#227A45") + arrow([(detail_x, detail_y + detail_h / 2), (resp_x + resp_w, detail_y + detail_h / 2)], color="#7E22CE") + + return resp_x, resp_y, resp_w, resp_h + + +def draw_note_and_end(resp_panel): + resp_x, resp_y, resp_w, resp_h = resp_panel + note_x, note_y, note_w, note_h = 520, 3545, 4160, 150 + draw.rounded_rectangle((note_x, note_y, note_x + note_w, note_y + note_h), radius=30, fill="#FFF1F3", outline="#C01048", width=5) + note = "关键注意:入口层缺少统一鉴权和白名单;客户端可通过 Type + Name/Param 触发 SQL 或存储过程能力,异常多数不记录日志。" + draw_multiline_center(note, note_x + note_w / 2, note_y + note_h / 2, note_w - 120, font_note, fill="#7A271A", line_gap=8) + + end_x, end_y = W - 560, H - 125 + draw_box(end_x, end_y, 420, 95, "请求结束", "#E8F1FF", "#2864B4", font=font_box_small, radius=45) + arrow([(resp_x + resp_w / 2, resp_y + resp_h), (resp_x + resp_w / 2, H - 125), (end_x - 210, H - 125)], color="#344054") + + footer = "源文件:working/MESCommonBase流程图.mmd PNG:working/MESCommonBase流程图.png" + fw, _ = text_size(footer, font_edge) + draw.text(((W - fw) / 2, H - 45), footer, font=font_edge, fill="#667085") + + +draw_title() +draw_top_flow() +panel_info = draw_branch_panel() +resp_panel = draw_detail_panels(panel_info) +draw_note_and_end(resp_panel) + +img.save(OUT, format="PNG", dpi=(220, 220), optimize=True) +print(OUT) +print(f"{W}x{H}") diff --git a/working/render_response_sqlwebcall_flowcharts.py b/working/render_response_sqlwebcall_flowcharts.py new file mode 100644 index 0000000..f9d60e5 --- /dev/null +++ b/working/render_response_sqlwebcall_flowcharts.py @@ -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-Disposition,BinaryWrite(bytes)。\n\n" + "上传/default:使用 Response.Write 写 JSON 字符串,格式由下游决定。\n\n" + "IP 查询:直接写 userIP,实际是文本。\n\n" + "Type=16 bytes 为 null 时直接 return,调用方可能收到空响应。", + 430, + 2245, + 2140, + font=d.font_note, + ) + + d.panel(2860, 2140, 2180, 660, "3. 外层异常兜底") + d.text_left( + "responseText 初始值为 NULL。\n\n" + "ProcessRequest 外层 catch(Exception err) 不记录日志、不改 HTTP 状态码,只写当前 responseText。\n\n" + "如果异常发生在下游调用前,通常返回 NULL;如果发生在赋值后,可能返回旧结果。", + 2930, + 2245, + 2040, + font=d.font_note, + ) + + d.note_box( + 680, + 2940, + 4040, + 150, + "主流程风险:响应格式混合(二进制/JSON/文本/NULL/空响应),Response.End 位于 try 内,default 分支执行能力取决于 SqlWebCall 内部 Type。", + ) + d.box(d.width - 520, 3180, 420, 92, "请求结束", "#E8F1FF", "#2864B4", radius=45, font=d.font_small) + d.arrow([(d.width / 2, 3090), (d.width - 730, 3180)]) + d.footer("源:MESCommonBase响应输出与SqlWebCall主流程图.mmd 图:MESCommonBase响应输出与SqlWebCall主流程图.png") + d.save("MESCommonBase响应输出与SqlWebCall主流程图.png") + + +def draw_secondary(): + d = Diagram(5800, 4300) + d.title("DataLink.SqlWebCall 默认分支次流程图", "次图:SqlWebCall 内部初始化、Type 二次分发、参数解析、数据库执行和返回格式") + + cx = d.width / 2 + d.box(cx, 285, 1260, 120, "MESCommonBase default 分支\nDataLink.SqlWebCall(type,jsonData,dataobj)", "#F3E8FF", "#7E22CE") + d.diamond(cx, 500, 520, 160, "initSystemIsOk?", "#FFF4CC", "#B27700") + d.box(4150, 500, 1040, 140, "InitSystemReg\n读取 Web.config ConnectionString\ninitSystemIsOk = true", "#E8F1FF", "#2864B4", font=d.font_small) + d.diamond(cx, 735, 500, 160, "switch\n(type)", "#FFF4CC", "#B27700") + d.arrow([(cx, 345), (cx, 420)]) + d.arrow([(cx + 260, 500), (3630, 500)], label="否", label_pos=(3430, 458)) + d.arrow([(4150, 570), (4150, 735), (3150, 735)]) + d.arrow([(cx, 580), (cx, 655)], label="是", label_pos=(2820, 625)) + + panel_y = 930 + d.panel(150, panel_y, 5500, 1630, "1. Type 二次分发与处理") + + cols = [ + (860, "登录/加密类\n8888 / 5001 / 5002", "#E8F1FF", "#2864B4"), + (2180, "旧协议存储过程\n1 / 2 / 5", "#EEF2FF", "#4F46E5"), + (3500, "新协议存储过程\n11 / 111 / 12 / 13 / 21", "#EAF7EF", "#227A45"), + (4820, "SQL/命令执行类\n1001 / 1002 / 3 / 4 / 7 / 22 / 3001", "#FFF7E6", "#B27700"), + ] + for x, header, fill, outline in cols: + d.box(x, 1140, 1180, 180, header, fill, outline, font=d.font_small) + + d.box(860, 1420, 1180, 290, "8888:读取 Name/name 并加密\n5001:Password MD5 后执行存储过程\n5002:解密密码、比对数据库密码、成功生成 token", "#E8F1FF", "#2864B4", font=d.font_small) + d.box(860, 1805, 1180, 300, "返回格式\n加密字符串\n用户表 JSON + token\n失败 result=0,msg=...", "#FFFFFF", "#2864B4", font=d.font_small) + d.arrow([(860, 1230), (860, 1275)]) + d.arrow([(860, 1565), (860, 1655)]) + + d.box(2180, 1420, 1180, 290, "输入对象:jsonobj dataobj\nParam 拼接字符串\nType 1/2: @p=value=type\nType 5: @p&value&type|...", "#EEF2FF", "#4F46E5", font=d.font_small) + d.box(2180, 1805, 1180, 300, "SQLCommon.GetCmdParam\n按 & / = / | 拆参数\n支持 output 参数\n随后 ExecuteStoredProcedure", "#FFFFFF", "#4F46E5", font=d.font_small) + d.arrow([(2180, 1230), (2180, 1275)]) + d.arrow([(2180, 1565), (2180, 1655)]) + + d.box(3500, 1420, 1180, 290, "GetString_JsonData\n读取 Type/Name/Param/token\nParam 是 JSON 数组字符串\n兼容大小写字段", "#EAF7EF", "#227A45", font=d.font_small) + d.diamond(3500, 1785, 440, 150, "token\n非空?", "#FFF4CC", "#B27700") + d.box(3030, 2045, 470, 150, "CheckToken\n失败返回\nresult=3", "#FFF1F3", "#C01048", font=d.font_small) + d.box(3970, 2045, 650, 150, "Param 转 SqlParameter[]\n数组值转逗号字符串\noutput=1 设输出参数", "#EAF7EF", "#227A45", font=d.font_small) + d.arrow([(3500, 1230), (3500, 1275)]) + d.arrow([(3500, 1565), (3500, 1710)]) + d.arrow([(3280, 1785), (3030, 1970)], label="是", label_pos=(3195, 1890)) + d.arrow([(3720, 1785), (3970, 1970)], label="否/通过", label_pos=(3880, 1890)) + + d.box(4820, 1420, 1180, 290, "Name/name 可能是 SQL 文本或表名\n1001/1002:带参数 SQL\n3/4/22:直接 SQL\n7:DROP/CREATE 表并导入", "#FFF7E6", "#B27700", font=d.font_small) + d.box(4820, 1805, 1180, 300, "SQLCommon.ExecuteDataTable / ExecuteDataset\nExecuteInsertMesWork / ExecuteSelectMesWork\n或 DbCallType1003_SqlCmd.SqlExec", "#FFFFFF", "#B27700", font=d.font_small) + d.arrow([(4820, 1230), (4820, 1275)]) + d.arrow([(4820, 1565), (4820, 1655)]) + + d.panel(300, 2740, 2440, 780, "2. 底层数据库执行链路") + d.text_left( + "存储过程:SQLCommon.ExecuteStoredProcedure\n- SqlCommand.CommandType = StoredProcedure\n- CommandTimeout = 0\n- SqlDataAdapter.Fill(DataSet/DataTable)\n\n" + "SQL 查询:ExecuteSelectMesWork / ExecuteDataTable / ExecuteDataset\n- SqlDataAdapter(sql, conn)\n- Fill(dt/ds)\n\n" + "SQL 增删改:ExecuteInsertMesWork / ExecuteNonQuery\n- SqlCommand.CommandText = 请求传入 SQL\n- ExecuteNonQuery / ExecuteScalar", + 380, + 2845, + 2290, + font=d.font_note, + ) + + d.panel(3060, 2740, 2440, 780, "3. 返回格式与响应出口") + d.text_left( + "DataTable JSON:Type 1/3/5/11/1002 常见。\n\n" + "result 标志:Type 2/4/12/1001 常见,成功 result=1,失败 result=0。\n\n" + "分页:Type 111 返回 { rows, total },total 来自 ItemCount 输出参数。\n\n" + "新结构:Type 21/22 返回 { code, message, data }。\n\n" + "未支持 Type:SqlWebCall 可能返回空字符串。", + 3140, + 2845, + 2290, + font=d.font_note, + ) + + d.note_box( + 560, + 3700, + 4680, + 210, + "次流程风险:token 多数是非空才校验;客户端可控制 Name/name;SQL 文本、存储过程名、表名缺少统一白名单;CommandTimeout=0 可能导致长时间阻塞。", + ) + d.box(d.width - 620, 4080, 560, 95, "返回 MESCommonBase\nResponse.Write", "#E8F1FF", "#2864B4", radius=45, font=d.font_small) + d.arrow([(d.width / 2, 3910), (d.width - 900, 4080)]) + d.footer("源:MESCommonBase响应输出与SqlWebCall次流程图.mmd 图:MESCommonBase响应输出与SqlWebCall次流程图.png") + d.save("MESCommonBase响应输出与SqlWebCall次流程图.png") + + +if __name__ == "__main__": + draw_main() + draw_secondary() diff --git a/working1/01-项目功能内容.md b/working1/01-项目功能内容.md new file mode 100644 index 0000000..1e70cb1 --- /dev/null +++ b/working1/01-项目功能内容.md @@ -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、数据库、耗时、结果。 diff --git a/working1/02-项目程序开发详细步骤.md b/working1/02-项目程序开发详细步骤.md new file mode 100644 index 0000000..4fb9986 --- /dev/null +++ b/working1/02-项目程序开发详细步骤.md @@ -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 +{ + 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> Rows { get; init; } = []; + public long Total { get; init; } + public int PageIndex { get; init; } + public int PageSize { get; init; } +} +``` + +错误中间件: + +```text +ExceptionHandlingMiddleware + -> 捕获异常 + -> 记录 traceId/user/action + -> 返回 ApiResponse +``` + +## 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 QueryAsync(DbExecutionContext context, CancellationToken ct); + Task ExecuteAsync(DbExecutionContext context, CancellationToken ct); + Task 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 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 Parameters { get; init; } = []; + public IReadOnlyDictionary Commands { get; init; } + = new Dictionary(); +} +``` + +执行流程: + +```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. 记录验收证据。 diff --git a/working1/03-推进台账.md b/working1/03-推进台账.md new file mode 100644 index 0000000..fd5dd4c --- /dev/null +++ b/working1/03-推进台账.md @@ -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`、解决方案和项目文件。 diff --git a/working1/04-任务矩阵.md b/working1/04-任务矩阵.md new file mode 100644 index 0000000..b5ebf82 --- /dev/null +++ b/working1/04-任务矩阵.md @@ -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、保留但强审计。 +- 默认生产环境不可被普通前台调用。 diff --git a/working1/05-验收证据.md b/working1/05-验收证据.md new file mode 100644 index 0000000..2ac6e45 --- /dev/null +++ b/working1/05-验收证据.md @@ -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 启动证据 | 待补充 | diff --git a/working1/06-决策记录.md b/working1/06-决策记录.md new file mode 100644 index 0000000..0292372 --- /dev/null +++ b/working1/06-决策记录.md @@ -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/`、本地压缩包、凭据文件和其他本地产物推送到云仓库。 + +原因: + +- 保持仓库聚焦于迁移工作本身。 +- 降低无关大文件和本地状态污染。 +- 避免把本地凭据文件一并入库。 + +影响: + +- 云仓库是迁移工作仓库,不是旧系统完整备份仓库。 +- 如果后续确需纳入某部分旧代码,应单独评估并更新范围控制规则。 diff --git a/working1/README.md b/working1/README.md new file mode 100644 index 0000000..83312a2 --- /dev/null +++ b/working1/README.md @@ -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。