# MES 通用 WebAPI 多数据库迁移技术路线 目标:参照当前 `MESCommonBase.ashx + DataLink.SqlWebCall` 项目,重新设计一个可运行在 Linux 上、以 WebAPI 对前台提供服务、同时支持 MySQL、PostgreSQL、SQL Server 的通用服务端项目。 说明:用户提到的 `PostPreSQL` 下文按 `PostgreSQL` 理解;`SqlServerd` 下文按 `SQL Server` 理解。 生成时间:2026-07-02 ## 1. 当前项目迁移背景 当前项目的核心模式是: ```text 前台请求 MESCommonBase.ashx | |-- 请求体或 param 中带 Type / Name / Param | |-- MESCommonBase.ashx 按 Type 做第一层分发 |-- 文件下载:2001/2002/2003/2004/16 |-- 文件上传:15 |-- IP 查询:4000 |-- 其他:DataLink.SqlWebCall(type,jsonData,dataobj) | |-- SqlWebCall 再按 Type 做第二层数据库能力分发 |-- 存储过程 |-- SQL 查询 |-- SQL 增删改 |-- 登录与 token |-- 分页 |-- 文件二进制 ``` 当前模式的优点: - 一个入口承载大量业务。 - 前台只需要传 `Type/Name/Param` 即可触发不同业务。 - 对 SQL Server 存储过程和动态 SQL 的支持比较直接。 当前模式的主要问题: - 客户端可控制 `Name/name`,也就是存储过程名、SQL 文本或表名。 - token 不是入口层强制校验,很多方法是“token 非空才校验”。 - 响应格式不统一,有二进制、数组 JSON、`{code,message,data}`、文本 IP、空响应、`NULL`。 - `ashx` 和 `.NET Framework` 不适合 Linux 原生运行。 - SQL Server 语义强耦合,迁移到 MySQL/PostgreSQL 时不能直接复用所有 SQL 和存储过程。 新项目的设计原则: - 兼容旧协议,但不继续扩大旧协议风险。 - WebAPI 化,运行在 Linux。 - 把数据库差异收敛到 Provider 策略层。 - 用白名单动作替代“前台任意传 SQL/存储过程名”。 - 统一鉴权、统一响应、统一日志、统一错误码。 - 对现有业务分阶段迁移,而不是一次性重写所有存储过程。 ## 2. 技术选型建议 ### 2.1 运行时 推荐: - `.NET 10 LTS` - `ASP.NET Core Web API` - Linux + Kestrel - Docker 或 systemd 部署 - Nginx 作为可选反向代理 原因: - .NET 10 是当前 LTS 版本,官方支持到 2028-11-14。 - .NET 8 LTS 在 2026-11-10 结束支持,不适合作为新项目长期基线。 - ASP.NET Core Web API 可跨平台运行,适合 Linux。 如果组织内部暂时无法升级 .NET 10,可用 `.NET 8` 做短期过渡,但路线中应明确升级窗口。 ### 2.2 数据访问方式 推荐主路径: - 核心动态执行层:`ADO.NET + 自定义 Provider 策略` - 简化参数和动态结果:可辅助使用 `Dapper` - 元数据、用户、权限、白名单配置:可使用 `EF Core` 不建议只用 EF Core 完成全部迁移,原因: - 当前项目大量能力是动态 SQL、动态存储过程、动态结果集、输出参数、二进制文件。 - EF Core 更适合实体模型 CRUD,不适合作为“通用 SQL/存储过程网关”的唯一执行层。 - 多数据库下存储过程、函数、输出参数和多结果集差异较大,必须有 provider-specific 策略。 推荐驱动: | 数据库 | 推荐驱动 | 用途 | | --- | --- | --- | | SQL Server | `Microsoft.Data.SqlClient` | SQL Server ADO.NET Provider | | PostgreSQL | `Npgsql` | PostgreSQL ADO.NET Provider | | MySQL/MariaDB | `MySqlConnector` | MySQL ADO.NET Provider | ### 2.3 API 风格 推荐: - Controller 风格 WebAPI,便于分组、版本化、过滤器、OpenAPI。 - 对外使用 REST-ish 接口,不继续暴露 `.ashx`。 - 保留一个 `legacy` 兼容接口接收旧 `Type/Name/Param` 请求。 - 新业务使用明确的 action id,例如 `quality.queryLineData`,不让前台直接传 SQL。 ## 3. 总体架构 建议采用分层架构: ```text Frontend | | HTTP/JSON, multipart/form-data, file download v ASP.NET Core WebAPI | |-- Controllers | |-- AuthController | |-- LegacyController | |-- ActionController | |-- FileController | |-- HealthController | |-- Application Layer | |-- ActionService | |-- LegacyTypeRouter | |-- FileService | |-- AuthService | |-- Domain / Contract | |-- ActionDefinition | |-- DbCommandRequest | |-- DbCommandResult | |-- ParameterDefinition | |-- UnifiedResponse | |-- Infrastructure |-- DatabaseProviderFactory |-- IDatabaseProvider |-- SqlServerProvider |-- PostgreSqlProvider |-- MySqlProvider |-- ConnectionStringResolver |-- AuditLogger ``` 核心思想: - Controller 不直接拼 SQL,不直接访问数据库。 - `LegacyTypeRouter` 负责兼容旧 Type。 - `ActionService` 负责执行已登记的动作。 - `IDatabaseProvider` 负责屏蔽 MySQL/PostgreSQL/SQL Server 差异。 - `ActionDefinition` 是白名单元数据,决定可执行什么 SQL 或存储过程。 ## 4. 推荐项目结构 ```text MesUniversalApi/ src/ MesUniversalApi.Api/ Controllers/ AuthController.cs LegacyController.cs ActionsController.cs FilesController.cs HealthController.cs Program.cs appsettings.json appsettings.Development.json MesUniversalApi.Application/ Services/ ActionService.cs LegacyTypeRouter.cs FileService.cs AuthService.cs Mapping/ LegacyParamParser.cs RequestNormalizer.cs Validation/ ActionRequestValidator.cs MesUniversalApi.Contracts/ Requests/ ExecuteActionRequest.cs LegacyExecuteRequest.cs DbParameterDto.cs Responses/ ApiResponse.cs QueryResultResponse.cs FileDownloadDescriptor.cs Enums/ DatabaseKind.cs CommandKind.cs MesUniversalApi.Domain/ Models/ ActionDefinition.cs ParameterDefinition.cs DataSourceDefinition.cs UserSession.cs MesUniversalApi.Infrastructure/ Database/ IDatabaseProvider.cs DatabaseProviderFactory.cs SqlServer/ SqlServerProvider.cs SqlServerDialect.cs PostgreSql/ PostgreSqlProvider.cs PostgreSqlDialect.cs MySql/ MySqlProvider.cs MySqlDialect.cs Security/ JwtTokenService.cs Observability/ AuditLogger.cs tests/ MesUniversalApi.Tests/ MesUniversalApi.IntegrationTests/ ``` ## 5. WebAPI 接口规划 ### 5.1 认证接口 ```http POST /api/v1/auth/login ``` 请求: ```json { "account": "admin", "password": "******" } ``` 响应: ```json { "code": "200", "message": "", "data": { "accessToken": "...", "expiresIn": 7200, "user": { "id": "1", "name": "admin" } } } ``` 建议: - 新系统使用 JWT Bearer。 - 不再依赖“token 非空才校验”的旧逻辑。 - 所有执行类接口默认要求授权。 ### 5.2 新动作执行接口 ```http POST /api/v1/actions/{actionId}/execute ``` 示例: ```http POST /api/v1/actions/quality.line.query/execute ``` 请求: ```json { "dataSource": "mes-main", "parameters": { "lineCode": "L01", "startTime": "2026-07-01 00:00:00", "endTime": "2026-07-02 00:00:00" }, "page": { "pageIndex": 1, "pageSize": 50 } } ``` 响应: ```json { "code": "200", "message": "", "data": { "rows": [], "total": 0 } } ``` 说明: - `actionId` 是白名单动作,不是 SQL 文本。 - `dataSource` 是已配置的数据源名。 - `parameters` 只允许传动作定义中声明的参数。 ### 5.3 旧协议兼容接口 ```http POST /api/v1/legacy/execute ``` 请求兼容旧格式: ```json { "type": "11", "name": "MES_工位_查询", "param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]", "token": "..." } ``` 实现策略: - 只作为迁移过渡接口。 - 默认仍需 JWT。 - 内部把 `type/name/param` 转换成 `LegacyCommandRequest`。 - `name` 必须匹配白名单,不能任意执行。 - 可通过配置逐步关闭高风险 Type,例如 `3/4/7/22/1001/1002/3001`。 ### 5.4 文件上传接口 ```http POST /api/v1/files/{actionId}/upload Content-Type: multipart/form-data ``` 表单字段: - `metadata`:JSON 字符串,包含业务参数。 - `file`:上传文件。 响应: ```json { "code": "200", "message": "", "data": { "fileId": "123", "fileName": "a.xlsx" } } ``` 建议: - 不再用“最后三个参数必须是 filename/suffix/bytes”的隐式约定。 - 在 action 定义中明确文件参数映射。 - 限制文件大小、扩展名、MIME。 - 大文件优先对象存储或文件系统,数据库只保存元数据;确需数据库保存时使用 BLOB/bytea/varbinary。 ### 5.5 文件下载接口 ```http GET /api/v1/files/{actionId}/download?fileId=123 ``` 响应: - 成功:`FileStreamResult` 或 `FileContentResult` - 失败:统一 JSON 错误 文件响应头: ```http Content-Type: application/octet-stream Content-Disposition: attachment; filename*=UTF-8''... Access-Control-Expose-Headers: Content-Disposition ``` ### 5.6 健康检查接口 ```http GET /health GET /health/ready GET /health/live ``` 检查内容: - API 进程是否存活。 - 数据源连接是否可用。 - Redis/配置中心等依赖是否可用。 ## 6. 数据库 Provider 抽象设计 ### 6.1 核心接口 ```csharp public interface IDatabaseProvider { DatabaseKind Kind { get; } Task QueryAsync(DbExecutionContext context, CancellationToken ct); Task ExecuteAsync(DbExecutionContext context, CancellationToken ct); Task ScalarAsync(DbExecutionContext context, CancellationToken ct); Task DownloadFileAsync(DbExecutionContext context, CancellationToken ct); } ``` ### 6.2 执行上下文 ```csharp public sealed class DbExecutionContext { public string DataSourceName { get; init; } = ""; public DatabaseKind DatabaseKind { get; init; } public CommandKind CommandKind { get; init; } public string CommandText { get; init; } = ""; public IReadOnlyList Parameters { get; init; } = []; public PageRequest? Page { get; init; } public int CommandTimeoutSeconds { get; init; } = 60; } ``` ### 6.3 命令类型 ```csharp public enum CommandKind { Text, StoredProcedure, Function, FileDownload, FileUpload } ``` 说明: - SQL Server:大量使用 `StoredProcedure`。 - MySQL:可使用 `StoredProcedure` 或 `CALL proc(...)`。 - PostgreSQL:查询型更建议封装为 `Function`,即 `select * from function(...)`;纯执行型可用 `procedure` 或 SQL。 ### 6.4 Provider Factory ```csharp public sealed class DatabaseProviderFactory { private readonly IReadOnlyDictionary _providers; public IDatabaseProvider Get(DatabaseKind kind) { if (_providers.TryGetValue(kind, out var provider)) return provider; throw new NotSupportedException($"Unsupported database kind: {kind}"); } } ``` ## 7. 多数据库差异处理 ### 7.1 参数名称差异 | 数据库 | 常见参数形式 | 建议内部规范 | | --- | --- | --- | | SQL Server | `@ParamName` | 内部使用无前缀名,Provider 添加 `@` | | MySQL | `@ParamName` 或 `?ParamName` | 内部使用无前缀名,Provider 统一转换 | | PostgreSQL | `@ParamName`、`:ParamName` 或位置参数 | 内部使用无前缀名,Provider 统一转换 | 内部 DTO: ```json { "name": "lineCode", "value": "L01", "type": "string", "direction": "input" } ``` 不要让前台关心 `@`、`:`、`?`。 ### 7.2 存储过程与函数差异 SQL Server: ```sql EXEC dbo.MES_Query @LineCode = @LineCode ``` PostgreSQL 推荐: ```sql select * from mes_query(@line_code); ``` MySQL: ```sql CALL mes_query(@lineCode); ``` 迁移策略: - 不要求三种数据库共用同一段 SQL。 - 对同一个 `actionId`,允许配置不同数据库的 `commandText`。 示例: ```json { "actionId": "quality.line.query", "commands": { "SqlServer": { "kind": "StoredProcedure", "text": "dbo.MES_Quality_LineQuery" }, "PostgreSql": { "kind": "Function", "text": "select * from mes_quality_line_query(@lineCode, @startTime, @endTime)" }, "MySql": { "kind": "Text", "text": "CALL mes_quality_line_query(@lineCode, @startTime, @endTime)" } } } ``` ### 7.3 分页差异 统一 API: ```json { "page": { "pageIndex": 1, "pageSize": 50 } } ``` Provider 翻译: | 数据库 | 分页写法 | | --- | --- | | SQL Server | `OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY` | | PostgreSQL | `LIMIT @PageSize OFFSET @Offset` | | MySQL | `LIMIT @Offset, @PageSize` 或 `LIMIT @PageSize OFFSET @Offset` | 建议: - 新 SQL 动作尽量由 API 层统一追加分页。 - 旧存储过程分页先兼容原有 `PageCurrent/PageSize/PageCount/ItemCount` 模式。 - 对大表必须要求排序字段,禁止无序分页。 ### 7.4 数据类型映射 | 逻辑类型 | SQL Server | PostgreSQL | MySQL | | --- | --- | --- | --- | | string | `nvarchar` | `text` / `varchar` | `varchar` / `text` | | int | `int` | `integer` | `int` | | long | `bigint` | `bigint` | `bigint` | | decimal | `decimal(p,s)` | `numeric(p,s)` | `decimal(p,s)` | | bool | `bit` | `boolean` | `tinyint(1)` / `boolean` | | datetime | `datetime2` | `timestamp` / `timestamptz` | `datetime` | | binary | `varbinary(max)` | `bytea` | `longblob` | | json | `nvarchar(max)` / JSON functions | `jsonb` | `json` | 建议内部参数类型: ```text string, int, long, decimal, bool, datetime, binary, json ``` ### 7.5 标识符差异 | 数据库 | 标识符引用 | | --- | --- | | SQL Server | `[TableName]` | | PostgreSQL | `"table_name"` | | MySQL | `` `table_name` `` | 要求: - 禁止前台直接传表名。 - 如果必须动态表名,必须来自白名单。 - Provider 负责引用标识符,不允许手工拼接未校验字符串。 ## 8. ActionDefinition 白名单设计 ### 8.1 为什么需要白名单 当前项目中 `Name/name` 可以是: - 存储过程名。 - SQL 查询文本。 - SQL 非查询文本。 - 表名。 这在通用项目里风险很高。新系统必须把“可执行什么”变成后端配置,而不是前台决定。 ### 8.2 白名单模型 ```csharp public sealed class ActionDefinition { public string ActionId { get; init; } = ""; public string DisplayName { get; init; } = ""; public string Module { get; init; } = ""; public bool Enabled { get; init; } public string[] RequiredRoles { get; init; } = []; public IReadOnlyList Parameters { get; init; } = []; public IReadOnlyDictionary Commands { get; init; } = new Dictionary(); } ``` 参数定义: ```csharp public sealed class ParameterDefinition { public string Name { get; init; } = ""; public string Type { get; init; } = "string"; public bool Required { get; init; } public int? MaxLength { get; init; } public object? DefaultValue { get; init; } public ParameterDirection Direction { get; init; } = ParameterDirection.Input; } ``` 命令定义: ```csharp public sealed class ProviderCommandDefinition { public CommandKind Kind { get; init; } public string Text { get; init; } = ""; public bool ReturnsRows { get; init; } public bool SupportsPaging { get; init; } } ``` ### 8.3 配置示例 ```json { "ActionId": "quality.line.query", "DisplayName": "产线质量查询", "Module": "Quality", "Enabled": true, "RequiredRoles": [ "quality.read" ], "Parameters": [ { "Name": "lineCode", "Type": "string", "Required": true, "MaxLength": 50 }, { "Name": "startTime", "Type": "datetime", "Required": true }, { "Name": "endTime", "Type": "datetime", "Required": true } ], "Commands": { "SqlServer": { "Kind": "StoredProcedure", "Text": "dbo.MES_Quality_LineQuery", "ReturnsRows": true, "SupportsPaging": true }, "PostgreSql": { "Kind": "Text", "Text": "select * from mes_quality_line_query(@lineCode, @startTime, @endTime)", "ReturnsRows": true, "SupportsPaging": true }, "MySql": { "Kind": "Text", "Text": "CALL mes_quality_line_query(@lineCode, @startTime, @endTime)", "ReturnsRows": true, "SupportsPaging": false } } } ``` 配置可以先放 JSON 文件,后续迁移到数据库表。 ## 9. 旧 Type 兼容设计 ### 9.1 Type 映射表 | 旧 Type | 新项目处理 | | --- | --- | | `2001/2002/2003` | 迁移为报表导出 action | | `2004/16` | 迁移为文件下载 action | | `15` | 迁移为文件上传 action | | `4000` | 独立 `/api/v1/client/ip` 或诊断接口 | | `1/2/5` | 旧协议存储过程兼容,但必须白名单 | | `11/111/12/13/21` | 新协议存储过程兼容,逐步改成 actionId | | `1001/1002/3/4/7/22/3001` | 高风险,默认禁用或仅内网管理端启用 | | `5001/5002/8888` | 迁移到 Auth/Password API,不继续放在通用 execute | ### 9.2 兼容请求转换 旧请求: ```json { "type": "11", "name": "MES_工位_查询", "param": "[{\"name\":\"@工位号\",\"value\":\"OP10\",\"type\":\"string\"}]" } ``` 转换为内部请求: ```json { "legacyType": 11, "legacyName": "MES_工位_查询", "actionId": "legacy.proc.MES_工位_查询", "parameters": { "工位号": "OP10" } } ``` 关键规则: - `legacyName` 必须能匹配白名单。 - 旧参数名中的 `@` 在内部去掉。 - 旧类型 `Int/String/Boolean/DateTime` 映射为新类型。 - 旧输出参数映射为 `ParameterDirection.Output`。 ## 10. 统一响应格式 建议所有 JSON 响应统一: ```json { "code": "200", "message": "", "data": {}, "traceId": "00-..." } ``` 分页: ```json { "code": "200", "message": "", "data": { "rows": [], "total": 0, "pageIndex": 1, "pageSize": 50 }, "traceId": "00-..." } ``` 错误: ```json { "code": "400", "message": "参数 lineCode 必填", "data": null, "traceId": "00-..." } ``` 旧接口兼容: - `/api/v1/legacy/execute` 可提供 `compatibilityMode=true`,短期返回旧格式。 - 默认建议返回新格式,并在 `data.legacyResult` 中保留旧结果。 ## 11. 文件上传下载迁移方法 ### 11.1 文件上传 当前旧逻辑: ```text Type=15 读取第一个文件 拆 name/suffix/bytes 把最后三个 SQL 参数覆盖为 filename/suffix/bytes 执行存储过程 ``` 新逻辑: ```text POST /api/v1/files/{actionId}/upload | |-- 鉴权 |-- 校验 actionId |-- 校验文件大小/扩展名/MIME |-- 校验 metadata 参数 |-- 按 action 定义映射文件字段 |-- Provider 执行数据库写入或对象存储写入 |-- 返回统一 JSON ``` ### 11.2 文件下载 当前旧逻辑: ```text Type=16/2004 调用存储过程 取 DataTable 第一行几列作为 fileName/suffix/bytes BinaryWrite ``` 新逻辑: ```text GET /api/v1/files/{actionId}/download | |-- 鉴权 |-- 校验 actionId 和参数 |-- Provider 查询文件元数据和内容 |-- 返回 FileContentResult 或 FileStreamResult ``` 不同数据库二进制字段: - SQL Server:`varbinary(max)` - PostgreSQL:`bytea` - MySQL:`longblob` 建议: - 小文件可数据库存储。 - 大文件使用对象存储或共享文件系统,数据库只保存路径、hash、大小、创建人、权限。 ## 12. 鉴权、授权与审计 ### 12.1 鉴权 推荐: - JWT Bearer。 - 登录后签发 access token。 - API 层统一 `[Authorize]`。 - 健康检查和登录接口例外。 ### 12.2 授权 按 action 授权: ```text 用户角色 -> 权限码 -> ActionDefinition.RequiredRoles ``` 示例: ```json { "actionId": "quality.line.query", "requiredRoles": [ "quality.read" ] } ``` ### 12.3 审计日志 每次执行记录: - `traceId` - 用户 ID - 来源 IP - actionId - dataSource - databaseKind - commandKind - 参数摘要,不记录敏感原文 - 执行耗时 - 返回行数 - 是否成功 - 错误码和错误摘要 审计日志建议写入独立表或日志平台。 ## 13. 配置设计 ### 13.1 appsettings 示例 ```json { "Database": { "DefaultDataSource": "mes-main", "DataSources": { "mes-main": { "Kind": "SqlServer", "ConnectionStringName": "MES_MAIN" }, "mes-pg": { "Kind": "PostgreSql", "ConnectionStringName": "MES_PG" }, "mes-mysql": { "Kind": "MySql", "ConnectionStringName": "MES_MYSQL" } } }, "Security": { "JwtIssuer": "MesUniversalApi", "JwtAudience": "MesFrontend", "AccessTokenMinutes": 120 }, "Cors": { "AllowedOrigins": [ "https://mes.example.com" ] } } ``` 连接字符串不建议写入仓库,推荐: - 环境变量。 - Docker secret。 - Kubernetes secret。 - 企业密钥管理服务。 ### 13.2 Linux 环境变量示例 ```bash export ConnectionStrings__MES_MAIN='Server=...;Database=...;User Id=...;Password=...;TrustServerCertificate=True' export ConnectionStrings__MES_PG='Host=...;Database=...;Username=...;Password=...' export ConnectionStrings__MES_MYSQL='Server=...;Database=...;User ID=...;Password=...' ``` ## 14. Linux 部署路线 ### 14.1 Docker 部署 推荐 Dockerfile: ```dockerfile FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app COPY publish/ . ENV ASPNETCORE_URLS=http://+:8080 EXPOSE 8080 ENTRYPOINT ["dotnet", "MesUniversalApi.Api.dll"] ``` 部署: ```bash dotnet publish -c Release -o publish docker build -t mes-universal-api:1.0.0 . docker run -d --name mes-api -p 8080:8080 --env-file .env mes-universal-api:1.0.0 ``` ### 14.2 systemd 部署 ```ini [Unit] Description=MES Universal API After=network.target [Service] WorkingDirectory=/opt/mes-api ExecStart=/usr/bin/dotnet /opt/mes-api/MesUniversalApi.Api.dll Restart=always RestartSec=5 Environment=ASPNETCORE_URLS=http://0.0.0.0:8080 Environment=ASPNETCORE_ENVIRONMENT=Production [Install] WantedBy=multi-user.target ``` ### 14.3 Nginx 反向代理 ```nginx server { listen 80; server_name mes-api.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## 15. 分阶段实施计划 ### 阶段 0:资产盘点 目标: - 列出当前所有前台调用的 `Type/Name/Param`。 - 统计文件上传下载接口。 - 统计直接 SQL 类 Type。 - 统计存储过程、表、视图、函数依赖。 输出: - `legacy_api_inventory.xlsx` - `stored_procedure_inventory.xlsx` - `sql_risk_list.md` ### 阶段 1:WebAPI 基础框架 目标: - 建立 ASP.NET Core WebAPI 项目。 - 配置 JWT、Swagger/OpenAPI、CORS、HealthCheck、日志。 - 建立统一响应结构。 - 建立 `IDatabaseProvider` 抽象。 验收: - Linux 上能启动。 - `/health` 正常。 - Swagger 可访问。 - 三种数据库至少能完成连接测试。 ### 阶段 2:SQL Server 兼容优先 目标: - 先支持当前 SQL Server 业务。 - 实现 `LegacyController`,兼容 `Type=11/12/13/15/16/2004`。 - 实现文件上传下载。 - 建立白名单。 策略: - 先不迁移数据库,只把入口从 `ashx` 换成 WebAPI。 - 前台可逐步切换地址。 验收: - 选 10 个高频接口完成回归。 - 返回数据与旧接口一致或可映射。 - 文件上传下载可用。 ### 阶段 3:PostgreSQL Provider 目标: - 实现 PostgreSQL 连接、查询、执行、文件二进制。 - 将选定业务动作迁移到 PostgreSQL function 或 SQL。 重点: - 存储过程返回结果集建议改 PostgreSQL function。 - 输出参数尽量改为结果列或 JSON 返回。 - 分页改为 `LIMIT/OFFSET`。 验收: - 同一个 `actionId` 可在 SQL Server 和 PostgreSQL 上运行。 - 对比测试结果一致。 ### 阶段 4:MySQL Provider 目标: - 实现 MySQL 连接、查询、执行、文件二进制。 - 将选定业务动作迁移到 MySQL procedure 或 SQL。 重点: - MySQL 存储过程和输出参数语义与 SQL Server 不同。 - 大量动态 SQL 应改为 provider-specific SQL 模板。 - 分页使用 `LIMIT/OFFSET`。 验收: - 同一个 `actionId` 可在 SQL Server、PostgreSQL、MySQL 上分别运行。 ### 阶段 5:安全治理与旧接口退场 目标: - 禁用高风险旧 Type。 - 前台从 `legacy/execute` 迁移到 `actions/{actionId}/execute`。 - 完成审计、限流、告警。 验收: - 不再允许前台传任意 SQL。 - 所有执行类接口都有 action 白名单和授权。 - 审计日志可追踪用户、动作、耗时和结果。 ## 16. 测试方法 ### 16.1 单元测试 覆盖: - 旧 `Param` 解析。 - 新 `Param` JSON 数组解析。 - 类型转换。 - 输出参数映射。 - action 白名单校验。 - 响应格式封装。 ### 16.2 集成测试 使用 Docker Compose 启动: - SQL Server - PostgreSQL - MySQL - API 对同一 action 执行: - 查询。 - 增删改。 - 分页。 - 文件上传。 - 文件下载。 - token 成功和失败。 ### 16.3 回归测试 从旧系统采样请求: ```text 旧请求 -> 旧 ashx 响应 旧请求 -> 新 legacy/execute 响应 对比字段、行数、文件 hash、错误码 ``` ### 16.4 性能测试 重点: - 大结果集查询。 - 分页查询。 - 大文件上传下载。 - 长事务或长存储过程。 - 数据库连接池。 工具: - k6 - JMeter - dotnet-counters - OpenTelemetry + Prometheus/Grafana ## 17. 关键实现方法 ### 17.1 请求标准化 ```text HTTP 请求 | |-- 新接口:actionId + parameters |-- 旧接口:type/name/param | v RequestNormalizer | |-- 参数名去前缀 @/:/? |-- 类型转换 |-- 默认值填充 |-- 必填校验 |-- 白名单校验 | v DbExecutionContext ``` ### 17.2 Provider 执行 ```text ActionService | |-- 根据 dataSource 找 DatabaseKind |-- 根据 actionId 找 ActionDefinition |-- 根据 DatabaseKind 选 command |-- DatabaseProviderFactory.Get(kind) |-- provider.QueryAsync / ExecuteAsync |-- 统一封装 ApiResponse ``` ### 17.3 动态结果序列化 建议: - 小结果集可先读入 `List>`。 - 大结果集使用 `DbDataReader` + `Utf8JsonWriter` 流式写出。 - 对日期、decimal、byte[] 做统一序列化策略。 ### 17.4 错误处理 统一中间件: ```text ExceptionHandlingMiddleware | |-- 捕获异常 |-- 记录 traceId、userId、actionId |-- 返回统一 JSON ``` 错误码建议: | code | 含义 | | --- | --- | | `200` | 成功 | | `400` | 参数错误 | | `401` | 未登录 | | `403` | 无权限 | | `404` | action 不存在或未启用 | | `409` | 业务冲突 | | `500` | 系统错误 | | `DB001` | 数据库连接失败 | | `DB002` | SQL/存储过程执行失败 | | `FILE001` | 文件不存在 | | `FILE002` | 文件大小或类型不允许 | ## 18. 数据库迁移策略 ### 18.1 不建议自动翻译所有 SQL SQL Server 到 PostgreSQL/MySQL 不能只靠字符串替换,差异包括: - 存储过程语法。 - 临时表。 - `TOP`、`OFFSET`、分页。 - `ISNULL`、`GETDATE`、字符串函数。 - `IDENTITY`、序列、自增。 - `nvarchar`、`bit`、`datetime2`、`varbinary(max)`。 - 事务和锁语义。 ### 18.2 推荐按 action 迁移 迁移粒度: ```text 一个 actionId = 一个可测试的业务能力 ``` 迁移步骤: 1. 固化旧 SQL Server 行为。 2. 编写 PostgreSQL/MySQL 版本 SQL 或函数。 3. 用相同测试输入比较输出。 4. 通过后把该 action 标记为多数据库支持。 ### 18.3 高风险 Type 处置 | Type | 风险 | 建议 | | --- | --- | --- | | `3` | 直接 SQL 查询 | 仅内网调试或禁用 | | `4` | 直接 SQL 非查询 | 禁用 | | `7` | 动态 DROP/CREATE 表 | 改为固定导入表或临时表 | | `22` | 直接 SQL 返回新结构 | 改为 actionId | | `1001/1002` | 客户端传 SQL | 改为 SQL 模板白名单 | | `3001` | SQL 命令执行入口 | 禁用或仅管理员审计使用 | ## 19. 交付物建议 第一批交付物: - 新 WebAPI 项目骨架。 - `IDatabaseProvider` 抽象。 - SQL Server Provider。 - PostgreSQL Provider。 - MySQL Provider。 - Legacy Type 11/12/13/15/16/2004 兼容。 - action 白名单配置。 - JWT 鉴权。 - 统一响应和异常处理。 - Dockerfile 和 Linux 部署文档。 - 集成测试环境。 第二批交付物: - 报表导出接口。 - 文件存储重构。 - 高风险 Type 替代方案。 - 审计日志和后台查询。 - 前台 API SDK 或 TypeScript client。 ## 20. 参考资料 - Microsoft .NET 支持策略: - EF Core 数据库 Provider 列表: - Npgsql PostgreSQL .NET Provider: - Npgsql EF Core Provider: - MySqlConnector 文档: - MySQL Connector/NET 官方文档: ## 21. 总结 本项目不建议简单把 `MESCommonBase.ashx` 改写成一个新的“万能 SQL WebAPI”。正确路线是: ```text 旧 Type 协议兼容 -> actionId 白名单 -> Provider 策略层 -> 统一鉴权和响应 -> SQL Server 先落地 -> PostgreSQL/MySQL 按 action 逐步迁移 -> 关闭高风险动态 SQL 能力 ``` 这样既能保持现有业务可迁移,又能支撑 Linux WebAPI 和多数据库目标,并为后续安全治理、审计、前台接口稳定性打基础。