1327 lines
30 KiB
Markdown
1327 lines
30 KiB
Markdown
# 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`
|
||
|
||
### 阶段 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<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 和多数据库目标,并为后续安全治理、审计、前台接口稳定性打基础。
|