Files
zszq-trs/项目文档/互换模块独立化最终方案.md
hjhan 497371ee97 feat(swap): 更新互换模块独立化方案文档
- 完善互换模块现状分析,明确新老表结构对比
- 更新核心架构原则,确定完全舍弃老表结构
- 优化业务逻辑实现,基于trade表和swap_position表构建
- 增强数据模型验证,添加必要的数据注解
- 完善异常处理机制,改进错误日志记录
- 重构API服务设计,实现完整的互换交易流程
- 更新实施路线图,明确各阶段任务优先级
- 删除冗余的迁移清单文档内容
2026-02-13 10:51:45 +08:00

717 lines
24 KiB
Markdown
Raw Permalink 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.
# 互换模块独立化最终实施方案
## 一、项目现状分析
### 1.1 技术架构特征
- **传统ASP.NET Core MVC**:基于Razor视图引擎的服务端渲染
- **jQuery+Bootstrap前端**:经典的Web表单交互模式
- **单体应用结构**:所有功能模块紧密耦合在同一应用中
- **共享数据库**:统一的数据存储和访问层
### 1.2 互换模块现状
```
互换相关组件:
├── Controllers (SwapTrade2Controller.cs - 现有主要控制器)
├── Views (40+个.cshtml文件,包括复杂表单和报表)
├── JavaScript (大量业务逻辑嵌入在页面脚本中)
├── Business Logic (SwapModule业务服务层)
└── Data Access (直接使用主应用DbContext)
表结构现状:
❌ 已废弃的老互换表:
├── trade_swap(互换交易主表)
├── trade_swap_flow(互换交易流水表)
└── trade_swap_flow_more(互换交易流水扩展表)
✅ 当前使用的现代化表结构:
├── trade(通用交易表,包含收益互换类型)
├── swap_flow(互换流水明细表)
├── swap_position(互换持仓表)
├── swap_flow_event(互换流水事件表)
├── swap_flow_merge(互换流水合并表)
├── swap_event(互换事件表)
├── eod_swap(互换日终表)
└── eod_swap_position(互换日终持仓归档表)
```
## 二、最终方案设计
### 2.1 核心架构原则
**完全舍弃老表结构** - 不再使用trade_swap系列废弃表
**基于新架构开发** - 围绕trade和swap_position等现代表构建
**前端零改动** - 保持所有.cshtml页面和JavaScript逻辑不变
**后端独立化** - 互换业务逻辑抽取为独立API服务
**共享数据库** - 继续使用现有数据库,避免数据迁移风险
**无感知切换** - 用户体验完全一致,业务连续性100%
### 2.2 最终架构图
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 前端页面 │ │ YARP代理 │ │ 互换API服务 │
│ (YLErpWeb) │◄──►│ (反向代理) │◄──►│ (YLSwapService) │
│ - .cshtml页面 │ │ - 路由智能转发 │ │ - 纯REST API │
│ - jQuery脚本 │ │ - 负载均衡 │ │ - 业务逻辑 │
│ - Razor渲染 │ │ - SSL终止 │ │ - 数据访问 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 共享数据库 │ │ 缓存中间件 │ │ 监控告警 │
│ (统一数据源) │ │ (Redis/内存) │ │ (Prometheus) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## 三、技术选型与实现
### 3.1 YARP反向代理配置(推荐)
#### 为什么选择YARP
**.NET原生** - 与现有技术栈完美契合
**高性能** - 比Nginx/Ocelot更好的性能表现
**灵活路由** - 支持复杂的路由匹配和转换规则
**易于维护** - 统一的.NET生态系统管理
#### YARP配置示例
```csharp
// Program.cs - YARP代理配置
var builder = WebApplication.CreateBuilder(args);
// 添加YARP反向代理
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
// 配置路由转发规则
app.MapReverseProxy(proxyPipeline =>
{
// 互换API路由 - 转发到独立服务
proxyPipeline.UseWhen(context =>
context.Request.Path.StartsWithSegments("/api/swap"),
handler =>
{
handler.UseMiddleware<AuthenticationMiddleware>(); // 认证前置
handler.UseSessionAffinity(); // 会话亲和性
});
// 前端页面路由 - 保持原有处理
proxyPipeline.UseWhen(context =>
context.Request.Path.StartsWithSegments("/swaptrade") ||
context.Request.Path.StartsWithSegments("/swaptrade2"),
handler =>
{
// 原有ASP.NET Core MVC处理逻辑
});
});
app.Run();
```
#### appsettings.json配置
```json
{
"ReverseProxy": {
"Routes": {
"swap-api-route": {
"ClusterId": "swap-backend",
"Match": {
"Path": "/api/swap/{**remainder}"
},
"Transforms": [
{ "PathRemovePrefix": "/api/swap" }
]
},
"swap-frontend-route": {
"ClusterId": "main-app",
"Match": {
"Path": "/swaptrade/{**remainder}"
}
}
},
"Clusters": {
"swap-backend": {
"Destinations": {
"swap1": {
"Address": "http://swap-service-1:8080/"
},
"swap2": {
"Address": "http://swap-service-2:8080/"
}
}
},
"main-app": {
"Destinations": {
"main": {
"Address": "http://main-application:80/"
}
}
}
}
}
}
```
### 3.2 独立互换服务实现
#### 项目结构
```
YLSwapService/
├── Controllers/
│ ├── SwapTradeController.cs # 交易API
│ ├── SwapPositionController.cs # 持仓API
│ └── SwapRiskController.cs # 风险API
├── Services/
│ ├── ISwapTradeService.cs # 业务接口
│ └── SwapTradeService.cs # 业务实现
├── DTOs/
│ ├── Requests/ # 请求数据传输对象
│ └── Responses/ # 响应数据传输对象
├── Infrastructure/
│ ├── Database/ # 数据访问配置
│ └── Messaging/ # 消息队列集成
└── Program.cs # 启动配置
```
#### 核心服务实现
```csharp
// Services/SwapTradeService.cs
public class SwapTradeService : ISwapTradeService
{
private readonly YLContext _context;
private readonly ILogger<SwapTradeService> _logger;
public SwapTradeService(YLContext context, ILogger<SwapTradeService> logger)
{
_context = context;
_logger = logger;
}
public async Task<TradeResultDto> CreateTradeAsync(CreateSwapTradeRequestDto request)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// 基于新架构的业务逻辑 - 使用trade表和swap_position表
var tradeNumber = await GenerateTradeNumberAsync();
// 创建主交易记录
var trade = new trade
{
TradeNumber = tradeNumber,
ClientId = request.ClientId,
ClientName = request.ClientName,
UnderlyingCode = request.UnderlyingCode,
TradeDate = request.TradeDate,
StartDate = request.StartDate,
ExerciseDate = request.ExerciseDate,
Notional = request.Notional,
SpotPrice = request.SpotPrice,
AssetBookName = request.AssetBookName,
TraderName = request.TraderName,
TradeType = "收益互换",
StructureType = "收益互换",
TradeStatus = "待确认",
ValidState = "Valid",
OptId = GetCurrentUserId(),
OptName = GetCurrentUserName(),
OptDate = DateTime.Now
};
_context.trade.Add(trade);
await _context.SaveChangesAsync();
// 创建互换持仓记录
var swapPosition = new swap_position
{
SwapTradeId = trade.id,
PositionType = request.PositionType, // 1:多头, 2:空头
UnderlyingCode = request.UnderlyingCode,
PosiQuantity = request.Quantity,
PosiNotionalValue = request.Notional,
PosiNetPrice = request.SpotPrice,
PosiStartDate = request.StartDate,
PosiMatuirityDate = request.ExerciseDate,
IsInitial = true,
OptId = GetCurrentUserId(),
OptName = GetCurrentUserName(),
OptTime = DateTime.Now
};
_context.swap_position.Add(swapPosition);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return new TradeResultDto
{
TradeId = trade.id,
TradeNumber = tradeNumber,
Status = "成功",
Message = "互换交易创建成功"
};
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "创建互换交易失败: {Message}", ex.Message);
throw new BusinessException("创建互换交易失败", ex);
}
}
public async Task<List<SwapTradeDto>> GetClientTradesAsync(int clientId, string status = "")
{
var query = _context.trade
.Where(t => t.ClientId == clientId
&& t.TradeType == "收益互换"
&& t.ValidState == "Valid");
if (!string.IsNullOrEmpty(status))
{
query = query.Where(t => t.TradeStatus == status);
}
var trades = await query
.OrderByDescending(t => t.TradeDate)
.ToListAsync();
return trades.Select(t => new SwapTradeDto
{
Id = t.id,
TradeNumber = t.TradeNumber,
ClientId = t.ClientId,
ClientName = t.ClientName,
UnderlyingCode = t.UnderlyingCode,
TradeDate = t.TradeDate.Value,
StartDate = t.StartDate.Value,
ExerciseDate = t.ExerciseDate.Value,
Notional = t.Notional ?? 0,
SpotPrice = t.SpotPrice ?? 0,
TradeStatus = t.TradeStatus,
CreatedAt = t.OptDate.Value
}).ToList();
}
}
```
#### 真实DTO数据结构
**交易相关DTO**
```csharp
// 创建互换交易请求DTO
public class CreateSwapTradeRequestDto
{
[Required]
public int ClientId { get; set; }
[Required]
public string ClientName { get; set; } = string.Empty;
[Required]
public string UnderlyingCode { get; set; } = string.Empty;
[Required]
public DateTime TradeDate { get; set; }
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime ExerciseDate { get; set; }
[Required]
[Range(0.01, double.MaxValue)]
public decimal Notional { get; set; }
[Required]
[Range(0.01, double.MaxValue)]
public decimal SpotPrice { get; set; }
[Required]
public string AssetBookName { get; set; } = string.Empty;
[Required]
public string TraderName { get; set; } = string.Empty;
[Required]
[Range(1, 2)]
public int PositionType { get; set; } // 1:多头, 2:空头
[Required]
[Range(0.01, double.MaxValue)]
public decimal Quantity { get; set; }
public string MetaData { get; set; } = "{}";
}
// 更新互换交易请求DTO
public class UpdateSwapTradeRequestDto
{
public decimal? Notional { get; set; }
public decimal? SpotPrice { get; set; }
public DateTime? ExerciseDate { get; set; }
public decimal? PayFixedRate { get; set; }
public decimal? GetFixedRate { get; set; }
public decimal? MarginRate { get; set; }
public string? MetaData { get; set; }
}
// 交易结果DTO
public class TradeResultDto
{
public int TradeId { get; set; }
public string TradeNumber { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
// 互换交易DTO
public class SwapTradeDto
{
public int Id { get; set; }
public string TradeNumber { get; set; } = string.Empty;
public int ClientId { get; set; }
public string ClientName { get; set; } = string.Empty;
public string UnderlyingCode { get; set; } = string.Empty;
public DateTime TradeDate { get; set; }
public DateTime StartDate { get; set; }
public DateTime ExerciseDate { get; set; }
public decimal Notional { get; set; }
public decimal SpotPrice { get; set; }
public string TradeStatus { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
// 互换交易详情DTO
public class SwapTradeDetailDto : SwapTradeDto
{
public string AssetBookName { get; set; } = string.Empty;
public string TraderName { get; set; } = string.Empty;
public string PayLongShort { get; set; } = string.Empty;
public decimal PayFixedRate { get; set; }
public decimal GetFixedRate { get; set; }
public string MarginRateType { get; set; } = string.Empty;
public decimal MarginRate { get; set; }
public string MetaData { get; set; } = "{}";
public List<SwapPositionDto> Positions { get; set; } = new();
}
```
**持仓相关DTO**
```csharp
// 互换持仓DTO
public class SwapPositionDto
{
public int Id { get; set; }
public int TradeId { get; set; }
public string TradeNumber { get; set; } = string.Empty;
public string UnderlyingCode { get; set; } = string.Empty;
public string UnderlyingName { get; set; } = string.Empty;
public decimal Quantity { get; set; }
public decimal Price { get; set; }
public decimal MarketValue { get; set; }
public decimal Pnl { get; set; }
public DateTime PositionDate { get; set; }
public string Status { get; set; } = string.Empty;
}
// 互换持仓详情DTO
public class SwapPositionDetailDto : SwapPositionDto
{
public decimal Delta { get; set; }
public decimal Gamma { get; set; }
public decimal Vega { get; set; }
public decimal Theta { get; set; }
public decimal Rho { get; set; }
public decimal MarginRequirement { get; set; }
public DateTime LastUpdateTime { get; set; }
}
// 持仓风险请求DTO
public class PositionRiskRequestDto
{
public List<int> PositionIds { get; set; } = new();
public string CalculationDate { get; set; } = DateTime.Today.ToString("yyyy-MM-dd");
public string ScenarioType { get; set; } = "基准情景";
}
// 持仓风险指标DTO
public class PositionRiskMetricsDto
{
public decimal TotalExposure { get; set; }
public decimal VaR { get; set; }
public decimal MaxLoss { get; set; }
public Dictionary<string, decimal> Greeks { get; set; } = new();
public DateTime CalculationTime { get; set; }
}
```
**互换交易控制器 (SwapTradeController)**
- `POST /api/SwapTrade` - 创建互换交易
- 请求体:CreateSwapTradeRequestDto
- 响应:TradeResultDto
- `GET /api/SwapTrade/{id}` - 获取交易详情
- 参数:int id (交易ID)
- 响应:SwapTradeDetailDto
- `GET /api/SwapTrade/client/{clientId}` - 查询客户互换交易列表
- 参数:int clientId (客户ID), string status (可选)
- 响应:List<SwapTradeDto>
- `PUT /api/SwapTrade/{id}` - 更新互换交易
- 参数:int id (交易ID), UpdateSwapTradeRequestDto request
- 响应:TradeResultDto
- `DELETE /api/SwapTrade/{id}` - 删除互换交易
- 参数:int id (交易ID)
- 响应:204 No Content
**互换持仓控制器 (SwapPositionController)**
- `GET /api/SwapPosition/client/{clientId}` - 获取客户持仓列表
- 参数:int clientId (客户ID)
- 响应:List<SwapPositionDto>
- `GET /api/SwapPosition/trade/{tradeId}` - 获取交易持仓详情
- 参数:int tradeId (交易ID)
- 响应:List<SwapPositionDetailDto>
- `POST /api/SwapPosition/risk/calculate` - 计算持仓风险指标
- 请求体:PositionRiskRequestDto
- 响应:PositionRiskMetricsDto
### 3.3 前端无感知适配
#### JavaScript调用自动转发
```javascript
// 前端调用保持原有方式不变
function createSwapTrade(formData) {
// 原有调用方式 - 保持不变
return $.post('/swaptrade/CreateTrade', formData);
// 通过YARP自动转发到 /api/SwapTrade
}
function querySwapTrades(clientId, status) {
// 原有调用方式 - 保持不变
return $.get(`/swaptrade/QueryTrades?clientId=${clientId}&status=${status || ''}`);
// 通过YARP自动转发到 /api/SwapTrade/client/{clientId}?status={status}
}
function getSwapTradeDetail(tradeId) {
// 原有调用方式 - 保持不变
return $.get(`/swaptrade/GetTradeDetail?id=${tradeId}`);
// 通过YARP自动转发到 /api/SwapTrade/{id}
}
function querySwapPositions(clientId) {
// 原有调用方式 - 保持不变
return $.get(`/swaptrade2/GetPosition?clientId=${clientId}`);
// 通过YARP自动转发到 /api/SwapPosition/client/{clientId}
}
function calculatePositionRisk(positionIds) {
// 原有调用方式 - 保持不变
return $.post('/swapposition/CalculateRisk', { positionIds: positionIds });
// 通过YARP自动转发到 /api/SwapPosition/risk/calculate
}
```
#### 路由映射规则
```csharp
// YARP路由转换规则
/*
原始路径 转发路径
/swaptrade/CreateTrade → /api/SwapTrade
/swaptrade/QueryTrades → /api/SwapTrade/client/{clientId}
/swaptrade/GetTradeDetail → /api/SwapTrade/{id}
/swaptrade2/GetPosition → /api/SwapPosition/client/{clientId}
/swapposition/CalculateRisk → /api/SwapPosition/risk/calculate
*/
```
## 四、关键技术决策说明
### 4.1 为什么使用YARP而不是Nginx/Ocelot
**优势对比:**
| 特性 | YARP | Nginx | Ocelot |
|------|------|-------|--------|
| .NET集成度 | ★★★★★ | ★★☆☆☆ | ★★★☆☆ |
| 配置复杂度 | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
| 性能表现 | ★★★★★ | ★★★★★ | ★★★☆☆ |
| 调试便利性 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 维护成本 | ★★★★★ | ★★☆☆☆ | ★★★☆☆ |
**选择理由:**
1. **技术栈一致性** - 全.NET生态,降低学习和维护成本
2. **开发调试友好** - 可以在Visual Studio中直接调试代理逻辑
3. **配置管理简单** - 统一的appsettings.json配置文件
4. **性能优异** - 微软官方优化,性能表现卓越
### 4.2 为什么MapRouteBeforeAuth很重要?
```csharp
// 关键配置:认证前置路由
proxyPipeline.UseWhen(context =>
context.Request.Path.StartsWithSegments("/api/swap"),
handler =>
{
// 在转发前进行认证检查
handler.UseMiddleware<AuthenticationMiddleware>();
// 确保安全性
handler.UseAuthorization();
});
```
**重要性:**
**安全保障** - 确保API调用经过身份验证
**权限控制** - 统一的权限检查机制
**审计追踪** - 完整的请求日志记录
**性能优化** - 避免不必要的后端调用
### 4.3 新架构表结构详解
**核心表关系说明:**
```
trade表 (主交易表)
├── TradeType = "收益互换" (标识互换交易)
├── id (主键,关联其他表)
└── 基础交易信息字段
swap_flow表 (互换流水明细表)
├── TradeId → trade.id (外键关联)
├── ClientId (客户ID)
├── UnderlyingCode (标的代码)
├── TradingQty (成交数量)
├── TradingAmount (成交金额)
├── TradingFee (交易费用)
└── TradeDate (交易日期)
swap_position表 (互换持仓表)
├── SwapTradeId → trade.id (外键关联)
├── PositionType (1:多头, 2:空头)
├── UnderlyingCode (标的代码)
├── PosiQuantity (持仓数量)
├── PosiNotionalValue (名义本金)
└── IsInitial (是否期初持仓)
swap_flow_event表 (流水事件表)
├── SwapTradeId → trade.id (外键关联)
├── PositionId → swap_position.id (外键关联)
├── EventType (1:开仓, 2:平仓)
├── EventDate (事件日期)
└── 数量和金额相关信息
eod_swap_position表 (日终持仓归档)
├── SwapTradeId → trade.id (外键关联)
├── PositionId → swap_position.id (外键关联)
├── ValueDate (归档日期)
└── 完整的持仓估值信息
```
### 4.4 为什么保持共享数据库?
**风险评估:**
**数据库拆分风险**
- 数据一致性难以保证
- 分布式事务复杂度高
- 迁移过程容易出错
- 维护成本显著增加
**共享数据库优势**
- 零数据迁移风险
- 保持现有业务逻辑不变
- 简化开发和测试流程
- 降低运维复杂度
## 七、实施路线图
### 5.1 阶段一:基础设施搭建(1-2周)
- [x] 确认完全舍弃老互换表结构(trade_swap系列)
- [ ] 创建YLSwapService独立项目
- [ ] 配置YARP反向代理
- [ ] 设置开发和测试环境
- [ ] 建立CI/CD流水线
### 5.2 阶段二:业务逻辑迁移(2-3周)
- [ ] 提取互换核心业务逻辑
- [ ] 实现RESTful API接口
- [ ] 配置路由转发规则
- [ ] 完成单元测试覆盖
### 5.3 阶段三:集成测试(1周)
- [ ] 端到端功能测试
- [ ] 性能基准测试
- [ ] 安全性验证
- [ ] 用户验收测试
### 5.4 阶段四:生产部署(1周)
- [ ] 灰度发布策略
- [ ] 监控告警配置
- [ ] 应急回滚预案
- [ ] 文档和培训
## 八、监控与运维
### 6.1 关键监控指标
```yaml
# Prometheus监控配置
metrics:
- name: swap_api_response_time
help: "互换API响应时间"
type: histogram
- name: swap_api_error_rate
help: "互换API错误率"
type: gauge
- name: yarp_proxy_requests_total
help: "YARP代理请求数"
type: counter
```
### 6.2 健康检查配置
```csharp
// 互换服务健康检查
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "database")
.AddRedis(redisConnectionString, name: "cache");
app.MapHealthChecks("/health", new HealthCheckOptions
{
Predicate = _ => true,
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
```
## 九、风险控制与回滚
### 7.1 风险缓解措施
**渐进式切换** - 新旧服务并行运行
**完整监控** - 实时性能和错误监控
**自动回滚** - 配置驱动的快速回滚机制
**数据备份** - 完整的数据备份策略
### 7.2 回滚方案
```bash
#!/bin/bash
# 一键回滚脚本
kubectl rollout undo deployment/yl-main-app
kubectl rollout undo deployment/yl-swap-service
```
## 十、总结
本方案通过以下关键设计确保互换模块独立化的同时保持前端无感知:
**彻底现代化** - 完全基于新架构,舍弃所有老互换表结构
**零业务中断** - 前端用户无感知,操作习惯完全不变
**技术先进性** - 采用微软官方推荐的YARP反向代理技术
**风险可控性** - 渐进式实施,完善的监控和回滚机制
**扩展性强** - 为未来微服务化奠定坚实基础
通过本次独立化改造,互换模块将在保持业务连续性的前提下,实现技术架构的现代化升级,为系统的长期发展提供强有力的技术支撑。
**特别说明:**
- **swap_flow表**作为互换流水明细的核心表,维护着每笔交易的详细流水信息,包括成交数量、金额、费用等关键数据
- 与**swap_position表**密切配合,实现完整的持仓管理和流水追踪
- 是连接前端交易录入和后端风险计算的重要数据桥梁
- **trade表**作为通用交易表,统一管理包括收益互换在内的所有交易类型