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

30 KiB
Raw Permalink Blame History

MES 通用 WebAPI 多数据库迁移技术路线

目标:参照当前 MESCommonBase.ashx + DataLink.SqlWebCall 项目,重新设计一个可运行在 Linux 上、以 WebAPI 对前台提供服务、同时支持 MySQL、PostgreSQL、SQL Server 的通用服务端项目。

说明:用户提到的 PostPreSQL 下文按 PostgreSQL 理解;SqlServerd 下文按 SQL Server 理解。

生成时间2026-07-02

1. 当前项目迁移背景

当前项目的核心模式是:

前台请求 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. 总体架构

建议采用分层架构:

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. 推荐项目结构

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 认证接口

POST /api/v1/auth/login

请求:

{
  "account": "admin",
  "password": "******"
}

响应:

{
  "code": "200",
  "message": "",
  "data": {
    "accessToken": "...",
    "expiresIn": 7200,
    "user": {
      "id": "1",
      "name": "admin"
    }
  }
}

建议:

  • 新系统使用 JWT Bearer。
  • 不再依赖“token 非空才校验”的旧逻辑。
  • 所有执行类接口默认要求授权。

5.2 新动作执行接口

POST /api/v1/actions/{actionId}/execute

示例:

POST /api/v1/actions/quality.line.query/execute

请求:

{
  "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
  }
}

响应:

{
  "code": "200",
  "message": "",
  "data": {
    "rows": [],
    "total": 0
  }
}

说明:

  • actionId 是白名单动作,不是 SQL 文本。
  • dataSource 是已配置的数据源名。
  • parameters 只允许传动作定义中声明的参数。

5.3 旧协议兼容接口

POST /api/v1/legacy/execute

请求兼容旧格式:

{
  "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 文件上传接口

POST /api/v1/files/{actionId}/upload
Content-Type: multipart/form-data

表单字段:

  • metadataJSON 字符串,包含业务参数。
  • file:上传文件。

响应:

{
  "code": "200",
  "message": "",
  "data": {
    "fileId": "123",
    "fileName": "a.xlsx"
  }
}

建议:

  • 不再用“最后三个参数必须是 filename/suffix/bytes”的隐式约定。
  • 在 action 定义中明确文件参数映射。
  • 限制文件大小、扩展名、MIME。
  • 大文件优先对象存储或文件系统,数据库只保存元数据;确需数据库保存时使用 BLOB/bytea/varbinary。

5.5 文件下载接口

GET /api/v1/files/{actionId}/download?fileId=123

响应:

  • 成功:FileStreamResultFileContentResult
  • 失败:统一 JSON 错误

文件响应头:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename*=UTF-8''...
Access-Control-Expose-Headers: Content-Disposition

5.6 健康检查接口

GET /health
GET /health/ready
GET /health/live

检查内容:

  • API 进程是否存活。
  • 数据源连接是否可用。
  • Redis/配置中心等依赖是否可用。

6. 数据库 Provider 抽象设计

6.1 核心接口

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 执行上下文

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 命令类型

public enum CommandKind
{
    Text,
    StoredProcedure,
    Function,
    FileDownload,
    FileUpload
}

说明:

  • SQL Server大量使用 StoredProcedure
  • MySQL可使用 StoredProcedureCALL proc(...)
  • PostgreSQL查询型更建议封装为 Function,即 select * from function(...);纯执行型可用 procedure 或 SQL。

6.4 Provider Factory

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

{
  "name": "lineCode",
  "value": "L01",
  "type": "string",
  "direction": "input"
}

不要让前台关心 @:?

7.2 存储过程与函数差异

SQL Server

EXEC dbo.MES_Query @LineCode = @LineCode

PostgreSQL 推荐:

select * from mes_query(@line_code);

MySQL

CALL mes_query(@lineCode);

迁移策略:

  • 不要求三种数据库共用同一段 SQL。
  • 对同一个 actionId,允许配置不同数据库的 commandText

示例:

{
  "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

{
  "page": {
    "pageIndex": 1,
    "pageSize": 50
  }
}

Provider 翻译:

数据库 分页写法
SQL Server OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
PostgreSQL LIMIT @PageSize OFFSET @Offset
MySQL LIMIT @Offset, @PageSizeLIMIT @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

建议内部参数类型:

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 白名单模型

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

参数定义:

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;
}

命令定义:

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 配置示例

{
  "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 兼容请求转换

旧请求:

{
  "type": "11",
  "name": "MES_工位_查询",
  "param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]"
}

转换为内部请求:

{
  "legacyType": 11,
  "legacyName": "MES_工位_查询",
  "actionId": "legacy.proc.MES_工位_查询",
  "parameters": {
    "工位号": "OP10"
  }
}

关键规则:

  • legacyName 必须能匹配白名单。
  • 旧参数名中的 @ 在内部去掉。
  • 旧类型 Int/String/Boolean/DateTime 映射为新类型。
  • 旧输出参数映射为 ParameterDirection.Output

10. 统一响应格式

建议所有 JSON 响应统一:

{
  "code": "200",
  "message": "",
  "data": {},
  "traceId": "00-..."
}

分页:

{
  "code": "200",
  "message": "",
  "data": {
    "rows": [],
    "total": 0,
    "pageIndex": 1,
    "pageSize": 50
  },
  "traceId": "00-..."
}

错误:

{
  "code": "400",
  "message": "参数 lineCode 必填",
  "data": null,
  "traceId": "00-..."
}

旧接口兼容:

  • /api/v1/legacy/execute 可提供 compatibilityMode=true,短期返回旧格式。
  • 默认建议返回新格式,并在 data.legacyResult 中保留旧结果。

11. 文件上传下载迁移方法

11.1 文件上传

当前旧逻辑:

Type=15
读取第一个文件
拆 name/suffix/bytes
把最后三个 SQL 参数覆盖为 filename/suffix/bytes
执行存储过程

新逻辑:

POST /api/v1/files/{actionId}/upload
  |
  |-- 鉴权
  |-- 校验 actionId
  |-- 校验文件大小/扩展名/MIME
  |-- 校验 metadata 参数
  |-- 按 action 定义映射文件字段
  |-- Provider 执行数据库写入或对象存储写入
  |-- 返回统一 JSON

11.2 文件下载

当前旧逻辑:

Type=16/2004
调用存储过程
取 DataTable 第一行几列作为 fileName/suffix/bytes
BinaryWrite

新逻辑:

GET /api/v1/files/{actionId}/download
  |
  |-- 鉴权
  |-- 校验 actionId 和参数
  |-- Provider 查询文件元数据和内容
  |-- 返回 FileContentResult 或 FileStreamResult

不同数据库二进制字段:

  • SQL Servervarbinary(max)
  • PostgreSQLbytea
  • MySQLlongblob

建议:

  • 小文件可数据库存储。
  • 大文件使用对象存储或共享文件系统数据库只保存路径、hash、大小、创建人、权限。

12. 鉴权、授权与审计

12.1 鉴权

推荐:

  • JWT Bearer。
  • 登录后签发 access token。
  • API 层统一 [Authorize]
  • 健康检查和登录接口例外。

12.2 授权

按 action 授权:

用户角色 -> 权限码 -> ActionDefinition.RequiredRoles

示例:

{
  "actionId": "quality.line.query",
  "requiredRoles": [ "quality.read" ]
}

12.3 审计日志

每次执行记录:

  • traceId
  • 用户 ID
  • 来源 IP
  • actionId
  • dataSource
  • databaseKind
  • commandKind
  • 参数摘要,不记录敏感原文
  • 执行耗时
  • 返回行数
  • 是否成功
  • 错误码和错误摘要

审计日志建议写入独立表或日志平台。

13. 配置设计

13.1 appsettings 示例

{
  "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 环境变量示例

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

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"]

部署:

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 部署

[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 反向代理

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 回归测试

从旧系统采样请求:

旧请求 -> 旧 ashx 响应
旧请求 -> 新 legacy/execute 响应
对比字段、行数、文件 hash、错误码

16.4 性能测试

重点:

  • 大结果集查询。
  • 分页查询。
  • 大文件上传下载。
  • 长事务或长存储过程。
  • 数据库连接池。

工具:

  • k6
  • JMeter
  • dotnet-counters
  • OpenTelemetry + Prometheus/Grafana

17. 关键实现方法

17.1 请求标准化

HTTP 请求
  |
  |-- 新接口actionId + parameters
  |-- 旧接口type/name/param
  |
  v
RequestNormalizer
  |
  |-- 参数名去前缀 @/:/?
  |-- 类型转换
  |-- 默认值填充
  |-- 必填校验
  |-- 白名单校验
  |
  v
DbExecutionContext

17.2 Provider 执行

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 错误处理

统一中间件:

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 不能只靠字符串替换,差异包括:

  • 存储过程语法。
  • 临时表。
  • TOPOFFSET、分页。
  • ISNULLGETDATE、字符串函数。
  • IDENTITY、序列、自增。
  • nvarcharbitdatetime2varbinary(max)
  • 事务和锁语义。

18.2 推荐按 action 迁移

迁移粒度:

一个 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. 参考资料

21. 总结

本项目不建议简单把 MESCommonBase.ashx 改写成一个新的“万能 SQL WebAPI”。正确路线是

旧 Type 协议兼容
  -> actionId 白名单
  -> Provider 策略层
  -> 统一鉴权和响应
  -> SQL Server 先落地
  -> PostgreSQL/MySQL 按 action 逐步迁移
  -> 关闭高风险动态 SQL 能力

这样既能保持现有业务可迁移,又能支撑 Linux WebAPI 和多数据库目标,并为后续安全治理、审计、前台接口稳定性打基础。