# 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 和多数据库目标，并为后续安全治理、审计、前台接口稳定性打基础。
