30 KiB
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 LTSASP.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
表单字段:
metadata:JSON 字符串,包含业务参数。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
响应:
- 成功:
FileStreamResult或FileContentResult - 失败:统一 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:可使用
StoredProcedure或CALL 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, @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 |
建议内部参数类型:
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 Server:
varbinary(max) - PostgreSQL:
bytea - MySQL:
longblob
建议:
- 小文件可数据库存储。
- 大文件使用对象存储或共享文件系统,数据库只保存路径、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.xlsxstored_procedure_inventory.xlsxsql_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解析。 - 新
ParamJSON 数组解析。 - 类型转换。
- 输出参数映射。
- 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 不能只靠字符串替换,差异包括:
- 存储过程语法。
- 临时表。
TOP、OFFSET、分页。ISNULL、GETDATE、字符串函数。IDENTITY、序列、自增。nvarchar、bit、datetime2、varbinary(max)。- 事务和锁语义。
18.2 推荐按 action 迁移
迁移粒度:
一个 actionId = 一个可测试的业务能力
迁移步骤:
- 固化旧 SQL Server 行为。
- 编写 PostgreSQL/MySQL 版本 SQL 或函数。
- 用相同测试输入比较输出。
- 通过后把该 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”。正确路线是:
旧 Type 协议兼容
-> actionId 白名单
-> Provider 策略层
-> 统一鉴权和响应
-> SQL Server 先落地
-> PostgreSQL/MySQL 按 action 逐步迁移
-> 关闭高风险动态 SQL 能力
这样既能保持现有业务可迁移,又能支撑 Linux WebAPI 和多数据库目标,并为后续安全治理、审计、前台接口稳定性打基础。