Files
MesUniversalApi-Migration/working/MES通用WebAPI多数据库迁移技术路线.md

1327 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<QueryResult> QueryAsync(DbExecutionContext context, CancellationToken ct);
Task<NonQueryResult> ExecuteAsync(DbExecutionContext context, CancellationToken ct);
Task<ScalarResult> ScalarAsync(DbExecutionContext context, CancellationToken ct);
Task<FileContentResultModel> 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<DbParameterValue> 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<DatabaseKind, IDatabaseProvider> _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<ParameterDefinition> Parameters { get; init; } = [];
public IReadOnlyDictionary<DatabaseKind, ProviderCommandDefinition> Commands { get; init; }
= new Dictionary<DatabaseKind, ProviderCommandDefinition>();
}
```
参数定义:
```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`
### 阶段 1WebAPI 基础框架
目标:
- 建立 ASP.NET Core WebAPI 项目。
- 配置 JWT、Swagger/OpenAPI、CORS、HealthCheck、日志。
- 建立统一响应结构。
- 建立 `IDatabaseProvider` 抽象。
验收:
- Linux 上能启动。
- `/health` 正常。
- Swagger 可访问。
- 三种数据库至少能完成连接测试。
### 阶段 2SQL Server 兼容优先
目标:
- 先支持当前 SQL Server 业务。
- 实现 `LegacyController`,兼容 `Type=11/12/13/15/16/2004`。
- 实现文件上传下载。
- 建立白名单。
策略:
- 先不迁移数据库,只把入口从 `ashx` 换成 WebAPI。
- 前台可逐步切换地址。
验收:
- 选 10 个高频接口完成回归。
- 返回数据与旧接口一致或可映射。
- 文件上传下载可用。
### 阶段 3PostgreSQL Provider
目标:
- 实现 PostgreSQL 连接、查询、执行、文件二进制。
- 将选定业务动作迁移到 PostgreSQL function 或 SQL。
重点:
- 存储过程返回结果集建议改 PostgreSQL function。
- 输出参数尽量改为结果列或 JSON 返回。
- 分页改为 `LIMIT/OFFSET`。
验收:
- 同一个 `actionId` 可在 SQL Server 和 PostgreSQL 上运行。
- 对比测试结果一致。
### 阶段 4MySQL 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<Dictionary<string, object?>>`。
- 大结果集使用 `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 支持策略:<https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core>
- EF Core 数据库 Provider 列表:<https://learn.microsoft.com/en-us/ef/core/providers/>
- Npgsql PostgreSQL .NET Provider<https://www.npgsql.org/>
- Npgsql EF Core Provider<https://www.npgsql.org/efcore/>
- MySqlConnector 文档:<https://mysqlconnector.net/>
- MySQL Connector/NET 官方文档:<https://dev.mysql.com/doc/connector-net/en/>
## 21. 总结
本项目不建议简单把 `MESCommonBase.ashx` 改写成一个新的“万能 SQL WebAPI”。正确路线是
```text
旧 Type 协议兼容
-> actionId 白名单
-> Provider 策略层
-> 统一鉴权和响应
-> SQL Server 先落地
-> PostgreSQL/MySQL 按 action 逐步迁移
-> 关闭高风险动态 SQL 能力
```
这样既能保持现有业务可迁移,又能支撑 Linux WebAPI 和多数据库目标,并为后续安全治理、审计、前台接口稳定性打基础。