feat(swaptrade): 添加收益结算日期验证和限制功能

- 新增 MaxIncomeValueDate 属性用于限制收益结算日期上限
- 实现 parseLocalDate 函数用于解析本地日期格式
- 在前端添加日期选择器的最大日期限制和验证逻辑
- 添加 isAfterMaxIncomeValueDate 和 validateIncomeValueDate 方法进行日期验证
- 在后端添加 GetMaxIncomeValueDate 方法计算最大收益结算日期
- 实现 ValidateIncomeValueDate 对收益结算日期进行服务器端验证
- 添加 NormalizeIncomeUnwindDate 方法统一处理平仓日期
- 在模型中新增 MaxIncomeValueDate 非映射属性用于前端显示
- 更新日期格式化函数确保日期字符串的一致性处理
This commit is contained in:
张名锐
2026-07-06 15:20:21 +08:00
parent 86825cb2a1
commit 6835a593b0
4 changed files with 96 additions and 7 deletions
@@ -130,6 +130,11 @@ namespace YLErp.DBModels
/// </summary>
public DateTime ValueDate { get; set; }
/// <summary>
/// 收益结算日期上限
/// </summary>
[NotMapped]
public DateTime? MaxIncomeValueDate { get; set; }
/// <summary>
/// 平仓/互换日期
/// </summary>
public DateTime? UnwindDate { get; set; }
+35 -2
View File
@@ -113,6 +113,11 @@ namespace YLErp.Modules.SwapModule
new TradeUnwindService(this).CloseReCheck_SetTrade(swapTradeId, isSwap, needCheck);
}
protected virtual DateTime GetMaxIncomeValueDate(trade td)
{
return QdpCalendarHelper.GetNonHolidayDefore(td.ExerciseDate.Value.AddDays(-1));
}
#endregion
public SwapDealService(OptUserInfo optUser) : base(optUser)
@@ -394,7 +399,8 @@ namespace YLErp.Modules.SwapModule
var positions = DbContext.swap_position.Where(x => x.SwapTradeId == tradeId && !x.Invalid);
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(td.UnderlyingCode);
List<int> eventTypes = new List<int>() { (int)SwapFlowEventTypeEnum., (int)SwapFlowEventTypeEnum. };
var dealDate = valuedateBLL.ValueDate < td.ExerciseDate.Value ? valuedateBLL.ValueDate : td.ExerciseDate.Value;
var maxIncomeValueDate = GetMaxIncomeValueDate(td);
var dealDate = valuedateBLL.ValueDate.Date > maxIncomeValueDate.Date ? maxIncomeValueDate : valuedateBLL.ValueDate;
// 收益结算不检查收盘限制
var tradeExtend = DbContext.trade_extend.FirstOrDefault(x => x.TradeId == tradeId);
td.trade_extend = tradeExtend;
@@ -425,7 +431,7 @@ namespace YLErp.Modules.SwapModule
unwindData.UnwindDate = dealDate;
floatEvent.UnwindDate = unwindData.UnwindDate;
floatEvent.EventDate = dealDate;
unwindData.PayDate = QdpCalendarHelper.GetNonHoliday(unwindData.UnwindDate.Value.AddDays(td.trade_extend.ExtendObj.SettlementRules));
unwindData.PayDate = valuedateBLL.ValueDate;
floatEvent.PayDate = unwindData.PayDate;
floatEvent.SwapTradeId = tradeId;
floatEvent.SwapTradeNo = td.TradeNumber;
@@ -464,6 +470,7 @@ namespace YLErp.Modules.SwapModule
}
unwindData.FlowEvents.Add(floatEvent);
}
unwindData.MaxIncomeValueDate = maxIncomeValueDate;
return unwindData;
}
/// <summary>
@@ -1645,6 +1652,8 @@ namespace YLErp.Modules.SwapModule
{
throw new ServiceException("未找到交易信息");
}
NormalizeIncomeUnwindDate(unwindData);
ValidateIncomeValueDate(unwindData, td);
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
ValidateFrontendPnL(unwindData, isIncome: true); // 只读校验告警,不阻断交易
ExecuteInTransaction(() =>
@@ -1683,6 +1692,11 @@ namespace YLErp.Modules.SwapModule
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
}
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
if (eventType == (int)SwapEventTypeEnum.)
{
NormalizeIncomeUnwindDate(swapEvent.unwindData);
ValidateIncomeValueDate(swapEvent.unwindData, td);
}
var flowList = FindFlowEventsByEventId(swapEvent.id);
string action = eventType == (int)SwapEventTypeEnum. ? ClientCashInCashOut._互换 : ClientCashInCashOut._平仓费;
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
@@ -1726,6 +1740,11 @@ namespace YLErp.Modules.SwapModule
{
throw new ServiceException("未找到交易信息");
}
if (eventType == (int)SwapEventTypeEnum.)
{
NormalizeIncomeUnwindDate(unwindData);
ValidateIncomeValueDate(unwindData, td);
}
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
string action = eventType == (int)SwapEventTypeEnum. ? ClientCashInCashOut._互换 : ClientCashInCashOut._平仓费;
ExecuteInTransaction(() =>
@@ -1735,6 +1754,20 @@ namespace YLErp.Modules.SwapModule
SaveAllChanges();
});
}
private void ValidateIncomeValueDate(UnwindData unwindData, trade td)
{
var maxIncomeValueDate = GetMaxIncomeValueDate(td).Date;
if (unwindData.ValueDate.Date > maxIncomeValueDate)
{
throw new ServiceException($"手动互换结算日期不能晚于当前交易结束日期T-1{maxIncomeValueDate:yyyy-MM-dd}");
}
}
private void NormalizeIncomeUnwindDate(UnwindData unwindData)
{
unwindData.UnwindDate = unwindData.ValueDate;
}
/// <summary>
/// 保存平仓/互换事件
/// </summary>
+11 -4
View File
@@ -11,10 +11,17 @@
@section JS
{
<script>
function formatDateText(value) {
return value ? value.toString().substr(0, 10) : "";
}
var model = @Json.Serialize(Model);
model.StartDate = model.StartDate ? model.StartDate.substr(0, 10) : "";
model.ValueDate = model.ValueDate ? model.ValueDate.substr(0, 10) : "";
model.PayDate = model.PayDate ? model.PayDate.substr(0, 10) : "";
model.StartDate = formatDateText(model.StartDate);
model.ValueDate = formatDateText(model.ValueDate);
model.PayDate = formatDateText(model.PayDate);
model.MaxIncomeValueDate = formatDateText(model.MaxIncomeValueDate);
if (model.ValueDate && model.MaxIncomeValueDate && model.ValueDate > model.MaxIncomeValueDate) {
model.ValueDate = model.MaxIncomeValueDate;
}
var isUseApproval = "@isUseApproval"=="True";
var g_isShowReCheckClose = "@isShowReCheckClose" == "True";
</script>
@@ -45,7 +52,7 @@
<td>起始日期</td>
<td> {{deal.StartDate}}</td>
<td>收益结算日期</td>
<td> <vue-datepicker :maxdate="maxUnwindDate" :mindate="minStartDate" :holiday="1" v-model="deal.ValueDate" v-on:input="setValueDate" /></td>
<td> <vue-datepicker ref="incomeValueDatePicker" :maxdate="maxUnwindDate" :mindate="minStartDate" :holiday="1" v-model="deal.ValueDate" v-on:input="setValueDate" /></td>
</tr>
<tr>
<td>支付日期</td>
@@ -9,6 +9,19 @@ const inputFormatDividend = Object.freeze({ precision: 2, append: '', negative:
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
let ValueDate = model.ValueDate;
let MaxIncomeValueDate = model.MaxIncomeValueDate ? model.MaxIncomeValueDate.substr(0, 10) : ValueDate;
function parseLocalDate(value) {
if (!value) {
return null;
}
var parts = value.substr(0, 10).split('-');
if (parts.length !== 3) {
return null;
}
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
}
const vue = new Vue({
el: '#vueDiv',
data: {
@@ -22,7 +35,7 @@ const vue = new Vue({
},
computed: {
maxUnwindDate() {
return ValueDate;
return MaxIncomeValueDate;
},
minStartDate() {
return this.deal.StartDate;
@@ -33,7 +46,32 @@ const vue = new Vue({
this.initDeal();
this.setValueDate();
},
mounted() {
this.$nextTick(() => {
var $incomeValueDatePicker = $(this.$refs.incomeValueDatePicker.$el);
$incomeValueDatePicker.datepicker("option", "maxDate", parseLocalDate(MaxIncomeValueDate));
if (this.isAfterMaxIncomeValueDate(this.deal.ValueDate)) {
this.validateIncomeValueDate(this.deal.ValueDate);
} else {
$incomeValueDatePicker.val(this.deal.ValueDate);
}
});
},
methods: {
isAfterMaxIncomeValueDate(valueDate) {
return valueDate && MaxIncomeValueDate && valueDate > MaxIncomeValueDate;
},
validateIncomeValueDate(valueDate) {
if (!this.isAfterMaxIncomeValueDate(valueDate)) {
return true;
}
main.message("手动互换结算日期不能晚于当前交易结束日期T-1:" + MaxIncomeValueDate);
this.deal.ValueDate = MaxIncomeValueDate;
this.deal.UnwindDate = MaxIncomeValueDate;
this.floatPosition.UnwindDate = MaxIncomeValueDate;
$(this.$refs.incomeValueDatePicker.$el).val(MaxIncomeValueDate);
return false;
},
getPriceScale() {
return this.multiplier == 100 ? 0.01 : 1;
},
@@ -93,6 +131,9 @@ const vue = new Vue({
},
setValueDate(e) {//修改平仓日期
if (e) {
if (!this.validateIncomeValueDate(e)) {
e = MaxIncomeValueDate;
}
this.deal.ValueDate = e;
this.deal.UnwindDate = e;
this.floatPosition.UnwindDate = e;
@@ -212,6 +253,9 @@ const vue = new Vue({
main.message("请输入平仓日期");
return;
}
if (!thisObj.validateIncomeValueDate(thisObj.deal.ValueDate)) {
return;
}
let reqObj = _.cloneDeep(thisObj.deal);
let marginCloneList = _.cloneDeep(thisObj.marginList);
reqObj.FlowEvents = _.cloneDeep(thisObj.interestList);