chore: initialize migration workspace
This commit is contained in:
526
working1/02-项目程序开发详细步骤.md
Normal file
526
working1/02-项目程序开发详细步骤.md
Normal file
@@ -0,0 +1,526 @@
|
||||
# 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. 记录验收证据。
|
||||
Reference in New Issue
Block a user