Files
MesUniversalApi-Migration/working1/02-项目程序开发详细步骤.md

527 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02-项目程序开发详细步骤
## 1. 开发总路线
项目推荐按以下顺序推进:
```text
准备阶段
-> 建立 WebAPI 骨架
-> 建立通用契约和响应
-> 建立数据库 Provider 抽象
-> 实现 SQL Server Provider
-> 实现 Legacy 兼容入口
-> 加入 actionId 白名单
-> 实现文件上传下载
-> 实现 PostgreSQL Provider
-> 实现 MySQL Provider
-> Linux 部署和回归测试
```
## 2. 技术栈
推荐:
- 语言C#
- 框架ASP.NET Core Web API
- 运行时:.NET 10 LTS
- 数据访问ADO.NET + Provider 策略;必要时配合 Dapper
- SQL Server 驱动:`Microsoft.Data.SqlClient`
- PostgreSQL 驱动:`Npgsql`
- MySQL 驱动:`MySqlConnector`
- 部署Linux + Docker 或 systemd
- 反向代理Nginx
- 文档OpenAPI/Swagger
- 日志Serilog 或 Microsoft.Extensions.Logging + OpenTelemetry
## 3. 新项目结构
建议结构:
```text
MesUniversalApi/
src/
MesUniversalApi.Api/
MesUniversalApi.Application/
MesUniversalApi.Contracts/
MesUniversalApi.Domain/
MesUniversalApi.Infrastructure/
tests/
MesUniversalApi.Tests/
MesUniversalApi.IntegrationTests/
```
当前移植根目录已先行创建为:
```text
MesUniversalApi/
src/
tests/
deploy/
docker/
systemd/
docs/
tools/
evidence/
```
职责:
| 项目 | 职责 |
| --- | --- |
| `Api` | Controller、鉴权、Swagger、过滤器、中间件 |
| `Application` | ActionService、LegacyTypeRouter、FileService、AuthService |
| `Contracts` | 请求和响应 DTO |
| `Domain` | ActionDefinition、DataSourceDefinition、业务模型 |
| `Infrastructure` | 数据库 Provider、日志、配置、JWT |
| `Tests` | 单元测试和集成测试 |
### 3.1 开工前置条件
开始 `T002 新 WebAPI 项目骨架创建` 之前,必须满足以下条件:
1. 开发机已安装 `.NET 10 SDK`
2. `dotnet --list-sdks` 输出中包含 `10.0.x`
3.`MesUniversalApi/` 根目录创建 `global.json`,锁定实际安装的 `10.0.x`
4. 不以 `net9.0` 或更低版本临时创建正式项目骨架。
5. 旧系统目录只用于盘点和对照,新增代码只进入 `MesUniversalApi/`
截至 2026-07-02当前机器仅检测到 `.NET SDK 9.0.311`,因此本轮只完成目录和文档准备,不执行 `net10.0` 项目生成。
## 4. 第一步:创建 WebAPI 骨架
建议命令:
```bash
cd MesUniversalApi
dotnet new globaljson --sdk-version 10.0.xxx
dotnet new sln -n MesUniversalApi
dotnet new webapi -f net10.0 -n MesUniversalApi.Api -o src/MesUniversalApi.Api
dotnet new classlib -f net10.0 -n MesUniversalApi.Application -o src/MesUniversalApi.Application
dotnet new classlib -f net10.0 -n MesUniversalApi.Contracts -o src/MesUniversalApi.Contracts
dotnet new classlib -f net10.0 -n MesUniversalApi.Domain -o src/MesUniversalApi.Domain
dotnet new classlib -f net10.0 -n MesUniversalApi.Infrastructure -o src/MesUniversalApi.Infrastructure
dotnet sln add src/**/*.csproj
```
说明:
- `10.0.xxx` 需替换为开发机实际安装的 `.NET 10 SDK` 版本号。
- 以上命令在 `.NET 10 SDK` 安装完成后执行。
- 当前轮次不以 `net9.0` 代替 `net10.0` 创建正式骨架。
依赖关系:
```text
Api -> Application -> Domain
Api -> Contracts
Application -> Contracts
Application -> Infrastructure abstractions
Infrastructure -> Domain / Contracts
```
## 5. 第二步:统一响应和异常处理
定义响应:
```csharp
public sealed class ApiResponse<T>
{
public string Code { get; init; } = "200";
public string Message { get; init; } = "";
public T? Data { get; init; }
public string TraceId { get; init; } = "";
}
```
分页响应:
```csharp
public sealed class PageResult
{
public IReadOnlyList<IDictionary<string, object?>> Rows { get; init; } = [];
public long Total { get; init; }
public int PageIndex { get; init; }
public int PageSize { get; init; }
}
```
错误中间件:
```text
ExceptionHandlingMiddleware
-> 捕获异常
-> 记录 traceId/user/action
-> 返回 ApiResponse<object>
```
## 6. 第三步:配置数据源
配置模型:
```csharp
public sealed class DataSourceDefinition
{
public string Name { get; init; } = "";
public DatabaseKind Kind { get; init; }
public string ConnectionStringName { get; init; } = "";
}
```
配置示例:
```json
{
"Database": {
"DefaultDataSource": "mes-main",
"DataSources": {
"mes-main": {
"Kind": "SqlServer",
"ConnectionStringName": "MES_SQLSERVER"
},
"mes-pg": {
"Kind": "PostgreSql",
"ConnectionStringName": "MES_POSTGRES"
},
"mes-mysql": {
"Kind": "MySql",
"ConnectionStringName": "MES_MYSQL"
}
}
}
}
```
连接字符串用环境变量:
```bash
ConnectionStrings__MES_SQLSERVER="Server=...;Database=...;User Id=...;Password=...;TrustServerCertificate=True"
ConnectionStrings__MES_POSTGRES="Host=...;Database=...;Username=...;Password=..."
ConnectionStrings__MES_MYSQL="Server=...;Database=...;User ID=...;Password=..."
```
## 7. 第四步:实现 Provider 抽象
核心接口:
```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);
}
```
执行上下文:
```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;
}
```
Provider 实现顺序:
1. `SqlServerProvider`
2. `PostgreSqlProvider`
3. `MySqlProvider`
## 8. 第五步SQL Server Provider
先支持 SQL Server原因
- 当前旧系统以 SQL Server 存储过程为主。
- 先迁移入口,降低一次性风险。
实现能力:
- Text 查询。
- Text 非查询。
- StoredProcedure 查询。
- StoredProcedure 非查询。
- 输出参数。
- DataSet/DataTable 动态 JSON。
- 文件二进制。
关键点:
- 使用 `Microsoft.Data.SqlClient`
- `CommandType.StoredProcedure` 用于旧存储过程。
- 参数名前缀统一由 Provider 处理。
- 命令超时不要照搬 `0`,默认建议 60 秒,可按 action 配置。
## 9. 第六步Legacy 兼容入口
Controller
```http
POST /api/v1/legacy/execute
```
流程:
```text
LegacyController
-> 接收 type/name/param
-> LegacyTypeRouter
-> 白名单检查
-> LegacyParamParser
-> DbExecutionContext
-> Provider 执行
-> ApiResponse
```
优先兼容:
- `11`
- `111`
- `12`
- `13`
- `15`
- `16`
- `2004`
高风险 Type 先禁用:
- `3`
- `4`
- `7`
- `22`
- `1001`
- `1002`
- `3001`
## 10. 第七步actionId 白名单
新增接口:
```http
POST /api/v1/actions/{actionId}/execute
```
动作定义:
```csharp
public sealed class ActionDefinition
{
public string ActionId { 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>();
}
```
执行流程:
```text
ActionController
-> ActionService
-> 读取 ActionDefinition
-> 校验权限
-> 校验参数
-> 根据 dataSource 选择 DatabaseKind
-> 根据 DatabaseKind 选择 CommandDefinition
-> Provider 执行
-> 统一响应
```
## 11. 第八步:文件上传下载
上传接口:
```http
POST /api/v1/files/{actionId}/upload
```
下载接口:
```http
GET /api/v1/files/{actionId}/download
```
开发步骤:
1. 定义文件 action。
2. 实现上传大小限制。
3. 实现扩展名白名单。
4. 实现 MIME 校验。
5. 实现数据库二进制写入。
6. 实现下载响应头。
7. 写文件 hash 校验测试。
二进制类型:
| 数据库 | 类型 |
| --- | --- |
| SQL Server | `varbinary(max)` |
| PostgreSQL | `bytea` |
| MySQL | `longblob` |
## 12. 第九步PostgreSQL Provider
实现内容:
- `NpgsqlConnection`
- 参数转换。
- Text 查询。
- Function 查询。
- 非查询执行。
- `bytea` 文件读写。
- `LIMIT/OFFSET` 分页。
迁移建议:
- SQL Server 查询型存储过程迁移为 PostgreSQL function。
- 输出参数尽量改为返回列或 JSON。
- 不做简单字符串替换,按 action 重写 SQL。
## 13. 第十步MySQL Provider
实现内容:
- `MySqlConnection`
- 参数转换。
- Text 查询。
- `CALL procedure(...)`
- 非查询执行。
- `longblob` 文件读写。
- `LIMIT/OFFSET` 分页。
注意:
- MySQL procedure 输出参数处理与 SQL Server 不同。
- 同一个 action 可为 MySQL 配置单独 SQL。
- 不要求 SQL Server 存储过程原样迁移。
## 14. 第十一步:鉴权与授权
实现:
- JWT 登录。
- `[Authorize]` 保护执行类接口。
- action 级权限。
- 用户角色映射。
- token 过期。
不沿用旧模式:
```text
token 非空才校验
```
新模式:
```text
除登录和健康检查外,默认必须鉴权
```
## 15. 第十二步:日志与审计
每次执行记录:
- traceId
- userId
- clientIp
- actionId
- legacyType
- legacyName
- dataSource
- databaseKind
- commandKind
- durationMs
- rowsAffected
- resultCode
- errorSummary
日志不记录明文密码和大二进制。
## 16. 第十三步Linux 部署
Docker
```bash
dotnet publish -c Release -o publish
docker build -t mes-universal-api:1.0.0 .
docker run -d -p 8080:8080 --env-file .env mes-universal-api:1.0.0
```
systemd
```ini
[Service]
WorkingDirectory=/opt/mes-api
ExecStart=/usr/bin/dotnet /opt/mes-api/MesUniversalApi.Api.dll
Restart=always
Environment=ASPNETCORE_URLS=http://0.0.0.0:8080
```
Nginx 反代:
```nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
## 17. 第十四步:测试
单元测试:
- 参数解析。
- 类型转换。
- action 白名单。
- 权限校验。
- 响应包装。
集成测试:
- SQL Server 查询。
- SQL Server 存储过程。
- PostgreSQL 查询。
- MySQL 查询。
- 文件上传下载。
- JWT 成功和失败。
回归测试:
```text
旧 ashx 响应
vs
新 legacy/execute 响应
```
性能测试:
- 大查询。
- 分页。
- 文件上传下载。
- 长存储过程。
- 连接池。
## 18. 首轮开发建议
第一轮只做最小闭环:
1. 安装并验证 `.NET 10 SDK`
2. 新建 WebAPI 项目。
3. 加统一响应。
4. 加 Swagger。
5. 加 SQL Server Provider。
6. 加一个 actionId 查询。
7. 加一个 legacy Type=11 查询。
8. Linux 本地或 Docker 启动。
9. 记录验收证据。