问题:重新收盘后用户手动互换产生的资金记录(ClientCashInCashOut, Action=系统操作-互换)被误删。根因 ClearSwapPositions:400 按 Action 字符串模糊删除,而该 Action 手动/自动共用,表无来源字段区分。 引入点 commit 44e89726(2026-05-14)。 - 新增决策文档(根因/引入时间/三种修复方案/自测方法) - 新增 SwapReEodDeleteManualCashRecordTest 4个测试(探查/诊断/录制红灯/离线校验) - 录制 golden 坐实 bug: trade 1903 手动互换资金记录 id=13032 会被误删
421 lines
22 KiB
Markdown
421 lines
22 KiB
Markdown
# 互换重收盘误删"手动互换"资金记录:问题分析与修复方案
|
||
|
||
> 本文分析互换交易"重新收盘/重算 EOD"后,**用户手动互换产生的资金记录被错误删除**的问题:根因、引入时间、三种修复方案与自测方法。
|
||
> 成文于 2026-07-01,排查范围覆盖 `5f97909f`(2026-04-29)至当前 HEAD。
|
||
|
||
---
|
||
|
||
## 一、问题概述
|
||
|
||
互换交易每次"重新收盘"(EOD 重算)时,会先清理旧数据再重算。当前实现会把**用户手动互换**产生的资金记录(`ClientCashInCashOut` 表中 `Action=系统操作-互换` 的记录)**一并删除**,导致历史资金流水丢失。
|
||
|
||
**复现条件**:一笔互换交易,用户已手动做过互换(EventType=3,生成 `Action=系统操作-互换` 资金记录),随后对该交易日重新收盘 → 该手动互换资金记录消失。
|
||
|
||
---
|
||
|
||
## 二、先澄清概念:哪些表是什么
|
||
|
||
| 表名 | 实体类 | 用途 | 与本问题关系 |
|
||
|------|--------|------|-------------|
|
||
| `ClientCashInCashOut` | `ClientCashInCashOut.cs:232` | **资金记录/出入金流水**(金额、方向、银行账户)| ⚠️ **被误删的就是这张表** |
|
||
| `swap_event` | `SwapEvent.cs` | 互换事件主记录(含 `ClientCashId` 外键)| 删除时按 EventType 过滤,安全 |
|
||
| `swap_flow_event` | `SwapFlowEvent.cs:14` | 开平仓事件明细(盈亏/计息),通过 `ClientCashId` 关联资金记录 | EOD 清理时按 EventType 过滤,**未误删** |
|
||
| ~~swap_capital_flow / swap_fund_flow~~ | 不存在 | —— | 代码库中无此表 |
|
||
|
||
**关键**:`swap_flow_event` 不是"资金记录",它和 `ClientCashInCashOut` 通过 `ClientCashId` 字段 1:1 关联。本问题中 `swap_flow_event` 没被误删,被误删的是 `ClientCashInCashOut`。
|
||
|
||
---
|
||
|
||
## 三、根因:删除逻辑按 Action 字符串过滤,无法区分手动/自动
|
||
|
||
### 问题代码位置
|
||
|
||
`YLErpDAL/Modules/SwapModule/SwapTradeBaseService.cs` 的 `ClearSwapPositions` 方法,**第 398–409 行**:
|
||
|
||
```csharp
|
||
// SwapTradeBaseService.cs:398-409(bug 所在)
|
||
var swapTradeIds = swapEvents.Select(s => s.SwapTradeId).Distinct().ToList();
|
||
var actions = new List<string>() { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 };
|
||
var premiumCashRecords = DbContext.ClientCashInCashOut
|
||
.Where(x => swapTradeIds.Contains(x.TradeId ?? 0)
|
||
&& x.HappenDate >= valueDate
|
||
&& actions.Contains(x.Action))
|
||
.ToList();
|
||
if (premiumCashRecords.Any())
|
||
{
|
||
DbContext.ClientCashInCashOut.RemoveRange(premiumCashRecords);
|
||
}
|
||
```
|
||
|
||
### 为什么会误删
|
||
|
||
`系统操作_互换` 这个 Action 字符串**手动互换和自动互换共用**,而 `ClientCashInCashOut` 表**没有任何字段区分手动 vs 自动来源**:
|
||
|
||
| 来源 | 写入位置 | Action 值 |
|
||
|------|---------|----------|
|
||
| 手动互换 `SwapIncome` | `SwapDealService.cs:1471` | `系统操作-互换` |
|
||
| 手动互换 `ApproveSwapTrade`(审批通过)| `SwapDealService.cs:1519` | `系统操作-互换` |
|
||
| 自动互换(利息腿)| `SwapEodPositionService.cs:516` | `系统操作-互换` |
|
||
| 自动互换(分红)| `SwapEodPositionService.cs:531` | `系统操作-互换` |
|
||
|
||
`AddClientCashInCashOut`(`SwapTradeBaseService.cs:285-318`)生成资金记录时**不设任何来源标记**。所以按 `TradeId + HappenDate>=valueDate + Action` 删除,无法过滤掉手动互换的记录。
|
||
|
||
> 注:第 391–396 行按 `swap_event.ClientCashId` 精准删除是**安全**的(只删自动互换 event 自己链接的那条)。**第 398–409 行的"按 Action 兜底模糊删除"才是误删元凶。**
|
||
|
||
---
|
||
|
||
## 四、引入时间线:是"修每次收盘多出一条"修出来的新 bug
|
||
|
||
本 bug 是修复"重复收盘导致互换收入重复计入"时,删除范围逐步扩大,最终修过头引入的。三次提交演进:
|
||
|
||
| 时间 | 提交 | 作者 | 改动 | 性质 |
|
||
|------|------|------|------|------|
|
||
| **2026-04-29** | `5f97909f` | hjhan | `fix(swap): 修复重复收盘导致互换收入(含预付金利息)重复计入的问题`——在删 `swap_event` 前按 `swap_event.ClientCashId` 精准删除关联资金记录 | ✅ **正确** |
|
||
| **2026-05-11** | `ebe711d3` | 吴方海 | `#EQD-5850 预付金返息的发生时间有问题`——自动互换把预付金返息单独拆条记录,并在删除时**新增按 `TradeId + 日期 + Action=预付金返息` 批删** | ⚠️ 首次把删除从"按主键ID"放宽到"按Action字符串" |
|
||
| **2026-05-14** | `44e89726` | 吴方海 | `收盘自动互换,需要将当前计息基数给回到持仓名义本金或固定值`——**把 `系统操作_互换` 加进删除 Action 列表** | ❌ **误删手动互换的直接引入点** |
|
||
|
||
`44e89726` 的 diff(实测验证)只改一行:
|
||
|
||
```diff
|
||
- && x.Action == ClientCashInCashOut.系统操作_预付金返息)
|
||
+ var actions = new List<string>() { ClientCashInCashOut.系统操作_预付金返息, ClientCashInCashOut.系统操作_互换 };
|
||
+ && actions.Contains(x.Action))
|
||
```
|
||
|
||
加进去的理由是"自动互换的互换主金额也要清掉重算",但没意识到 `系统操作_互换` 跟手动互换共用。
|
||
|
||
---
|
||
|
||
## 五、约束分析:为什么不能简单回退到 `5f97909f`
|
||
|
||
`5f97909f` 的精准删除(按 `swap_event.ClientCashId`)在**当时**是对的,但现在的自动互换逻辑已演进——**一次自动互换会生成多条资金记录**:
|
||
|
||
`SaveAutoSwapDeal`(`SwapEodPositionService.cs:506`)按条件最多生成 3 条 `ClientCashInCashOut`:
|
||
- `:516` 利息腿 `系统操作_互换`(`SwapCloseAmount != 0` 时)
|
||
- `:522` 预付金 `系统操作_预付金返息`(`SwapMarginRebatePnl != 0` 时)
|
||
- `:531` 分红 `系统操作_互换`(`SwapDividendPnl != 0` 时)
|
||
|
||
但 `swap_event.ClientCashId`(`:535`)**只指向最后一条非 0 的 id**,前面的被覆盖。所以单纯按 `ClientCashId` 删,会**漏删**自动互换的前几条资金记录 → 复发"每次收盘多出一条"老 bug(`5f97909f` 当时修的那个)。
|
||
|
||
> 手动互换也有同样结构:`SwapIncome:1471` 生成主金额(id 存进 event),`:1474` 预付金返息的 id 被丢弃。手动互换若被精准删除只删主金额那条,预付金返息会成"孤儿"残留——但手动互换**本就不该被重收盘删除**,所以这反而是"正确残留"。
|
||
|
||
---
|
||
|
||
## 六、修复方案(三选一,供团队决策)
|
||
|
||
### 方案 A(推荐,最小改动 + 治本):按 swap_event 链 + EventType 区分
|
||
|
||
**思路**:删除自动互换资金记录时,**只删与自动互换 swap_event 关联的记录**,不碰手动互换的 swap_event(EventType=3/互换)。
|
||
|
||
删除条件从"按 Action 字符串"改为"按 `swap_event.ClientCashId` 集合",并扩展 `swap_event` 与资金记录的关联(让被覆盖的前几条也能被找到):
|
||
|
||
```csharp
|
||
// 修复后(SwapTradeBaseService.cs:398-409 替换)
|
||
// 注意:本方法入参 eventTypes 调用方只传了 {自动互换}(SwapEodPositionService.cs:92),
|
||
// 所以 swapEvents 只含自动互换事件,手动互换事件不在内 —— 这就是安全的关键。
|
||
var swapEventIds = swapEvents.Select(s => s.id).ToList();
|
||
if (swapEventIds.Any())
|
||
{
|
||
// 已有:按 swap_event.ClientCashId 精准删除主金额那条(保留第391-396行不变)
|
||
|
||
// 新增:按 swap_flow_event.ClientCashId 反查自动互换生成的全部资金记录
|
||
// 自动互换的每条 swap_flow_event 都带 ClientCashId(指向其资金记录)
|
||
var autoCashIds = DbContext.swap_flow_event
|
||
.Where(x => swapEventIds.Contains(x.EventId ?? 0) && x.ClientCashId > 0)
|
||
.Select(x => x.ClientCashId.Value)
|
||
.Distinct()
|
||
.ToList();
|
||
// 合并 swap_event.ClientCashId(主金额)
|
||
var eventCashIds = swapEvents.Where(s => s.ClientCashId > 0).Select(s => s.ClientCashId.Value);
|
||
autoCashIds = autoCashIds.Union(eventCashIds).ToList();
|
||
|
||
if (autoCashIds.Any())
|
||
{
|
||
var autoCashRecords = DbContext.ClientCashInCashOut
|
||
.Where(x => autoCashIds.Contains(x.id))
|
||
.ToList();
|
||
DbContext.ClientCashInCashOut.RemoveRange(autoCashRecords);
|
||
}
|
||
}
|
||
// ❌ 删除原来的"按 Action 字符串模糊删除"整段(第398-409行)
|
||
```
|
||
|
||
**⚠️ 关键前提缺陷(已确认)**:当前 `SaveAutoSwapDeal:538-542` 生成 flow_event 时**只设了 `EventId`,没有给 `ClientCashId` 赋值**:
|
||
|
||
```csharp
|
||
// SwapEodPositionService.cs:538-542(现状,缺 ClientCashId 赋值)
|
||
flowEvents.ForEach(x =>
|
||
{
|
||
x.EventId = swapEvent.id;
|
||
DbContext.swap_flow_event.Add(x);
|
||
});
|
||
```
|
||
|
||
对比平仓路径 `ComposePage:685-686` 是有赋值的(`x.ClientCashId = clientCashId`)。所以**方案 A 必须同时补这个赋值**才能落地,否则反查 `swap_flow_event.ClientCashId` 会漏(复发"每次收盘多出一条"老 bug)。这是方案 A 的硬性前置改动,见第七节"改动 2"。
|
||
|
||
**优点**:
|
||
- 删除范围严格限定在自动互换事件链内,手动互换零误伤。
|
||
- 不新增字段,不改数据库结构。
|
||
- 语义清晰:删哪个 event,就删它关联的资金记录。
|
||
|
||
**风险**:
|
||
- 依赖 `swap_flow_event.ClientCashId` 的赋值完整性——当前自动互换分支未设,需补(改动 2)。
|
||
- **资金记录与 flow_event 不是 1:1**(已确认):
|
||
- `:420` 普通自动互换:生成 2 条资金记录(利息汇总 `:516` + 预付金汇总 `:522`),flow_event 是利息明细腿(多条)。
|
||
- `:498` 分红型自动互换:生成 1 条资金记录(分红汇总 `:531`),flow_event 是分红明细腿(多条)。
|
||
- 所以一条资金记录会被多条 flow_event 引用——改动 2 给 flow_event 设 ClientCashId 时,让同一笔互换的所有明细腿都指向该次生成的资金记录 id(利息腿指向 `:516` 的 id,预付金腿指向 `:522` 的 id)。删除时 `Distinct()` 去重即可。
|
||
- 由于 `clientCashId` 变量在 `SaveAutoSwapDeal:516/522/531` 被多次覆盖,最终 `swap_event.ClientCashId`(`:535`)只存最后一条。**改动 2 必须在每次 `AddClientCashInCashOut` 后立即把 id 赋给对应的 flow_event 组**,不能依赖最后的 `clientCashId` 值。需配套单测兜底(见第八节)。
|
||
|
||
---
|
||
|
||
### 方案 B(保守):保留 Action 过滤 + 加来源标记字段
|
||
|
||
**思路**:给 `ClientCashInCashOut` 加一个来源字段区分手动/自动,删除时排除手动。
|
||
|
||
```csharp
|
||
// 1. 数据库迁移:ClientCashInCashOut 加列
|
||
ALTER TABLE ClientCashInCashOut ADD COLUMN Source VARCHAR(20) DEFAULT '手动';
|
||
|
||
// 2. AddClientCashInCashOut 增加来源参数(SwapTradeBaseService.cs:285)
|
||
public int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate, string source = "手动")
|
||
{
|
||
...
|
||
ee.Source = source; // 新增
|
||
...
|
||
}
|
||
|
||
// 3. 自动互换调用处传 source="自动"(SwapEodPositionService.cs:516/522/531)
|
||
|
||
// 4. 删除过滤加排除条件(SwapTradeBaseService.cs:401-404)
|
||
.Where(x => swapTradeIds.Contains(x.TradeId ?? 0)
|
||
&& x.HappenDate >= valueDate
|
||
&& actions.Contains(x.Action)
|
||
&& x.Source == "自动") // 新增:只删自动生成的
|
||
```
|
||
|
||
**优点**:删除逻辑最直白,改一处过滤条件即可。
|
||
|
||
**缺点**:
|
||
- 需要数据库迁移(生产加列),风险高。
|
||
- 历史数据 `Source` 为默认值"手动",但历史自动互换记录也是 NULL/默认 → 需数据回填脚本,否则老 bug 数据残留。
|
||
- `Source` 字段语义需长期维护。
|
||
|
||
---
|
||
|
||
### 方案 C(最长远,工程量大):用 swap_event 作为唯一删除入口
|
||
|
||
**思路**:所有资金记录都必须挂到某个 `swap_event` 上(通过 `swap_flow_event.ClientCashId` 或新增 `ClientCashInCashOut.SwapEventId`),删除时一律按 event 链,彻底废弃按 Action 字符串删除。
|
||
|
||
**优点**:语义最清晰,根除"按 Action 字符串删除"这类隐患。
|
||
|
||
**缺点**:改动面大,需梳理所有生成资金记录的路径,确保都有 event 挂载。适合作为长期重构目标,不适合当下快速止血。
|
||
|
||
---
|
||
|
||
### 方案对比
|
||
|
||
| 维度 | 方案A(推荐)| 方案B | 方案C |
|
||
|------|-------------|-------|-------|
|
||
| 改动量 | 小(1个方法 + flow_event赋值补全)| 中(加列+迁移+多处调用)| 大(全链路重构)|
|
||
| 数据库迁移 | 否 | **是** | 视实现 |
|
||
| 治本程度 | 高 | 高 | 最高 |
|
||
| 风险 | 需确认 flow_event.ClientCashId 完整性 | 迁移风险+历史数据回填 | 改动面大 |
|
||
| 止血速度 | 快 | 慢 | 慢 |
|
||
|
||
---
|
||
|
||
## 七、具体小修复代码(方案 A)
|
||
|
||
### 改动 1:`SwapTradeBaseService.cs:398-409` 替换删除逻辑
|
||
|
||
```csharp
|
||
// ❌ 删除原第398-409行(按 Action 字符串模糊删除)
|
||
// ✅ 替换为:按自动互换 swap_event 链精准删除
|
||
|
||
// 自动互换一次可能生成多条资金记录(利息腿/预付金/分红),
|
||
// 但 swap_event.ClientCashId 只指向最后一条。通过 swap_flow_event.ClientCashId
|
||
// 反查自动互换事件关联的全部资金记录,避免误伤手动互换(EventType=3)。
|
||
var autoFlowCashIds = DbContext.swap_flow_event
|
||
.Where(x => eventIds.Contains(x.EventId ?? 0) && x.ClientCashId > 0)
|
||
.Select(x => x.ClientCashId.Value)
|
||
.Distinct()
|
||
.ToList();
|
||
var autoEventCashIds = swapEvents.Where(s => s.ClientCashId > 0).Select(s => s.ClientCashId.Value).ToList();
|
||
autoFlowCashIds.AddRange(autoEventCashIds);
|
||
autoFlowCashIds = autoFlowCashIds.Distinct().ToList();
|
||
|
||
if (autoFlowCashIds.Any())
|
||
{
|
||
var autoCashRecords = DbContext.ClientCashInCashOut
|
||
.Where(x => autoFlowCashIds.Contains(x.id))
|
||
.ToList();
|
||
DbContext.ClientCashInCashOut.RemoveRange(autoCashRecords);
|
||
}
|
||
```
|
||
|
||
> 说明:`eventIds` 已在第 377 行算好(`swapEvents.Select(s => s.id)`)。本方法入参 `eventTypes` 在重收盘入口(`SwapEodPositionService.cs:92`)只传 `{自动互换}`,所以 `swapEvents` 只含自动互换事件,手动互换事件天然不在删除范围内 —— 这是安全的根本保证。
|
||
|
||
### 改动 2(前提):补全 `swap_flow_event.ClientCashId` 赋值
|
||
|
||
自动互换 `SaveAutoSwapDeal`(`SwapEodPositionService.cs:506`)当前生成 flow_event 时只设了 `EventId`,没设 `ClientCashId`(`:538-542`、`:549-553`)。需补上,让方案 A 反查能找到全部资金记录。
|
||
|
||
由于资金记录是"汇总"的(利息汇总/预付金汇总/分红汇总各 1 条),而 flow_event 是"明细腿"(多条),赋值规则是:**同一笔互换的明细腿都指向该次生成的资金记录 id**。需要在 `SaveAutoSwapDeal` 内用独立变量保存每条资金记录 id(不能依赖被覆盖的 `clientCashId`):
|
||
|
||
```csharp
|
||
// SwapEodPositionService.cs:512-532 改造(用独立变量分别记录每个资金记录 id)
|
||
int interestCashId = 0; // 利息腿资金记录 id
|
||
int premiumCashId = 0; // 预付金腿资金记录 id
|
||
int dividendCashId = 0; // 分红资金记录 id
|
||
|
||
if (unwindData.SwapCloseAmount != 0)
|
||
{
|
||
interestCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapCloseAmount), ClientCashInCashOut.系统操作_互换, cashHappenDate);
|
||
}
|
||
if (unwindData.SwapMarginRebatePnl != 0)
|
||
{
|
||
premiumCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||
}
|
||
unwindData.SwapCloseAmount = unwindData.SwapRealizedPnL;
|
||
if (unwindData.SwapDividendPnl != 0)
|
||
{
|
||
var dividendPayDate = (dividendEvents != null && dividendEvents.Count > 0)
|
||
? dividendEvents.First().PayDate.Value : unwindData.ValueDate;
|
||
dividendCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapDividendPnl), ClientCashInCashOut.系统操作_互换, dividendPayDate);
|
||
}
|
||
|
||
// 保留 clientCashId 给 swap_event(取最后一条非0,与原行为一致)
|
||
int clientCashId = dividendCashId != 0 ? dividendCashId : (premiumCashId != 0 ? premiumCashId : interestCashId);
|
||
```
|
||
|
||
然后在 flow_event 赋值处按腿类型设 `ClientCashId`:
|
||
|
||
```csharp
|
||
// SwapEodPositionService.cs:536-555(flowEvents 是利息明细腿,指向利息资金记录)
|
||
if (flowEvents != null)
|
||
{
|
||
flowEvents.ForEach(x =>
|
||
{
|
||
x.EventId = swapEvent.id;
|
||
x.ClientCashId = interestCashId; // 补:利息明细腿指向利息汇总资金记录
|
||
DbContext.swap_flow_event.Add(x);
|
||
});
|
||
UpdateInitalPostion(flowEvents, td.id);
|
||
}
|
||
// dividendEvents 是分红明细腿,指向分红资金记录
|
||
if (dividendEvents != null)
|
||
{
|
||
dividendEvents.ForEach(x =>
|
||
{
|
||
x.EventId = swapEvent.id;
|
||
x.ClientCashId = dividendCashId; // 补:分红明细腿指向分红资金记录
|
||
DbContext.swap_flow_event.Add(x);
|
||
});
|
||
UpdateInitalPostion(dividendEvents, td.id);
|
||
}
|
||
```
|
||
|
||
> ⚠️ 预付金资金记录 `premiumCashId` 没有对应的 flow_event 可挂载(预付金是单独汇总的,无明细腿)。它只能靠 `swap_event.ClientCashId`(当它是最后一条时)或方案 A 删除逻辑里单独处理。**实施时需验证:当只有预付金非0时,`swap_event.ClientCashId` 是否指向预付金那条**(按上面 `clientCashId` 三元表达式,`premiumCashId != 0` 时会取它,✓ 覆盖)。
|
||
|
||
---
|
||
|
||
## 八、如何自测
|
||
|
||
### 自测 1:SQL 验证(最快,纯查询)
|
||
|
||
找一笔**当天有手动互换操作**的互换交易,重收盘前后对比资金记录:
|
||
|
||
```sql
|
||
-- 步骤1:盘前记录手动互换资金记录的 id 快照
|
||
SELECT id, TradeId, HappenDate, Action, Money, State, OptName, OptTime
|
||
FROM ClientCashInCashOut
|
||
WHERE TradeId = <手动互换过的交易ID>
|
||
AND Action = '系统操作-互换'
|
||
ORDER BY id;
|
||
-- 记下这些 id(比如 id 列表 = 1001, 1002)
|
||
|
||
-- 步骤2:对该交易日执行重收盘(前端操作或调 SwapPositionCompose)
|
||
|
||
-- 步骤3:盘后再查同样的 id
|
||
SELECT id, TradeId, Action, Money
|
||
FROM ClientCashInCashOut
|
||
WHERE id IN (1001, 1002); -- 替换为步骤1记下的 id
|
||
```
|
||
|
||
**判断标准**:
|
||
- **修复前(bug)**:步骤3 查不到记录(被 `RemoveRange` 删除)。
|
||
- **修复后(正确)**:步骤3 记录原样存在(id/Money/OptName 不变)。
|
||
|
||
### 自测 2:守恒校验(验证没修过头 → 自动互换仍能清理)
|
||
|
||
确保修复后**自动互换的重复资金记录仍被清理**,否则"每次收盘多出一条"老 bug 复发:
|
||
|
||
```sql
|
||
-- 对一笔纯自动互换交易,连续重收盘 3 次,每次收盘后查记录数
|
||
SELECT COUNT(*) AS cnt
|
||
FROM ClientCashInCashOut
|
||
WHERE TradeId = <自动互换交易ID>
|
||
AND Action IN ('系统操作-互换', '系统操作-预付金返息')
|
||
AND HappenDate >= '<收盘日>';
|
||
```
|
||
|
||
**判断**:连跑 3 次后 `cnt` 应**稳定不变**(每次清掉重算),**不应**线性递增(若递增说明自动互换资金漏删,老 bug 复发)。
|
||
|
||
### 自测 3:录制测试(复用已有 golden 模式,可进回归)
|
||
|
||
参考本仓库 `UnitTestProject/Modules/SwapModule/SwapDividendGoldenRecordTest.cs` 的录制模式(连真实测试库 `192.168.2.96/glms_yltrs_ylcms`),骨架:
|
||
|
||
```csharp
|
||
[Ignore] // 有写副作用,标 Ignore 不进 CI;手动跑
|
||
[TestCategory("DBRecording")]
|
||
public void ReEod_ShouldNotDeleteManualSwapCashRecord()
|
||
{
|
||
int tradeId = <有手动互换的交易ID>;
|
||
DateTime settleDate = <收盘日>;
|
||
|
||
using var ctx = DbContextFactory.GetYLDbContext();
|
||
|
||
// 盘前快照:手动互换资金记录 id
|
||
var manualCashIds = ctx.ClientCashInCashOut
|
||
.Where(x => x.TradeId == tradeId
|
||
&& x.Action == ClientCashInCashOut.系统操作_互换
|
||
&& x.HappenDate >= settleDate)
|
||
.Select(x => x.id).ToList();
|
||
Console.WriteLine($"盘前手动互换资金记录数: {manualCashIds.Count}");
|
||
|
||
// 触发重收盘
|
||
var service = new SwapEodPositionService(userInfo);
|
||
service.SwapPositionCompose(settleDate, preSettleDate, new List<int>{ clientId });
|
||
|
||
// 盘后验证:手动互换记录必须原样保留
|
||
var survivors = ctx.ClientCashInCashOut
|
||
.Where(x => manualCashIds.Contains(x.id)).ToList();
|
||
Assert.AreEqual(manualCashIds.Count, survivors.Count,
|
||
"手动互换资金记录不应被重收盘删除");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 九、团队决策问题
|
||
|
||
1. **选哪个方案?** 推荐方案 A(最小改动+治本),但需先确认 `swap_flow_event.ClientCashId` 在自动互换路径的赋值完整性。方案 B 需数据库迁移,方案 C 适合长期。
|
||
|
||
2. **历史被误删的数据怎么办?** 若生产已发生"手动互换资金记录被重收盘删除",是否需要从 `clientcashincashout_history`(`YLContext.cs:127`)或备份恢复?需业务确认影响范围。
|
||
|
||
3. **是否补单测兜底?** 当前 Swap 模块缺少"重收盘不误删手动数据"的单测,建议补自测 3 的录制测试进回归,防止 `44e89726` 类问题复发。
|
||
|
||
---
|
||
|
||
## 附录:关键代码位置索引
|
||
|
||
| 项 | 文件:行号 |
|
||
|----|----------|
|
||
| 删除逻辑(bug 所在)| `SwapTradeBaseService.cs:374`(ClearSwapPositions),第 398-409 行 |
|
||
| 重收盘调用入口 | `SwapEodPositionService.cs:94`(SwapPositionCompose,`removeEventTyps={自动互换}`, `delAfter=true`)|
|
||
| 资金记录生成(手动互换)| `SwapDealService.cs:1471`(SwapIncome)/ `:1519`(ApproveSwapTrade)|
|
||
| 资金记录生成(自动互换)| `SwapEodPositionService.cs:516`(利息腿)/ `:522`(预付金)/ `:531`(分红)|
|
||
| `AddClientCashInCashOut`(不设来源标记)| `SwapTradeBaseService.cs:285-318` |
|
||
| `ClientCashInCashOut` 实体(无来源字段)| `ClientCashInCashOut.cs:232` |
|
||
| 引入 bug 的提交 | `44e89726`(2026-05-14,把 `系统操作_互换` 加进删除 Action 列表)|
|
||
| 安全的对照基线 | `5f97909f`(2026-04-29,按 ClientCashId 精准删除)|
|