Files
zszq-trs/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs
T
tengyufan 20e4b82ae3 fix: 修复国联收益互换交易编号首次生成大小写不一致
- 首次入库后按 UpperTradeNumber 配置统一交易编号大小写
- 避免自动生成小写编号在后续编辑保存时被转为大写
- 补充交易编号大小写配置的单元测试
2026-07-30 17:46:18 +08:00

155 lines
6.1 KiB
C#
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.
using YLErp.BLL;
using YLErp.Configuration;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.TradeModule.DocGenerateModule
{
/// <summary>
/// 国贸交易确认书编号生成器
/// 规则:
/// - 对客交易(OpponentRole为"甲方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔对客交易(数字递增)}-{标的代码},从01开始
/// 例:GLMS-IS-20260424-01-250210IB
/// - 非对客交易(OpponentRole为"乙方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔非对客交易使用字母递增}-{标的代码},从A开始
/// 例:GLMS-IS-20260424-A-250210IB
/// </summary>
public class GuolianContractNoGenerator
{
private static readonly object _syncLock = new object();
public static bool IsGuolianSwapTrade(trade trade)
{
return IsGuolianSwapTrade(trade, PS.Config.Company);
}
public static bool IsGuolianSwapTrade(trade trade, CompanyEnum company)
{
return company == CompanyEnum.国联
&& trade?.TradeType == "收益互换";
}
public static bool ShouldGenerateTradeNumberAfterSave(trade trade, CompanyEnum company)
{
return IsGuolianSwapTrade(trade, company)
&& string.IsNullOrWhiteSpace(trade.TradeNumber);
}
public static bool CanReuseRejectedTradeNumber(trade trade, CompanyEnum company)
{
return IsGuolianSwapTrade(trade, company);
}
/// <summary>
/// 交易首次入库后按国联交易确认书编号规则回写交易编号。
/// </summary>
/// <returns>是否生成了交易编号</returns>
public static bool TryGenerateTradeNumberAfterSave(YLContext dbContext, trade trade)
{
if (!IsGuolianSwapTrade(trade) || !string.IsNullOrWhiteSpace(trade.TradeNumber))
{
return false;
}
if (trade.id <= 0)
{
throw new InvalidOperationException("国联收益互换交易编号必须在交易入库后生成");
}
var clientCode = DataCacheProvider.GetClientDataSource().GetData(trade.ClientId)?.Code;
if (string.IsNullOrWhiteSpace(clientCode))
{
throw new ServiceException("对手方代码缩写缺失,请联系运营组同事维护");
}
trade.TradeNumber = NormalizeTradeNumberCase(
Generate(dbContext, trade, clientCode),
PS.Config.ErpElement.UpperTradeNumber);
return true;
}
public static string NormalizeTradeNumberCase(string tradeNumber, bool upperTradeNumber)
{
return upperTradeNumber && !string.IsNullOrEmpty(tradeNumber)
? tradeNumber.ToUpperInvariant()
: tradeNumber;
}
/// <summary>
/// 按国联确认书编号规则拼装编号(纯函数,不依赖数据库,便于单测)。
/// 格式:GLMS-{clientCode}-{成交日期(yyyyMMdd)}-FICC-{序号}-{标的代码(去点)}
/// - 对客交易(甲方):序号用两位数字,从 01 开始
/// - 非对客交易(乙方):序号用字母递增,从 A 开始
/// </summary>
public static string BuildTradeNumber(trade trade, string clientCode, int sequenceNo)
{
var isClientTrade = trade.OpponentRole == "甲方";
var underlyingCode = (trade.UnderlyingCode ?? "").Replace(".", ""); // 去掉标的代码中的点号
var tradeDateStr = (trade.TradeDate ?? DateTime.MinValue).ToString("yyyyMMdd");
var seq = isClientTrade ? sequenceNo.ToString("D2") : NumberToLetter(sequenceNo);
return $"GLMS-{clientCode}-{tradeDateStr}-FICC-{seq}-{underlyingCode}";
}
/// <summary>
/// 生成国贸交易确认书编号
/// </summary>
/// <param name="dbContext">数据库上下文</param>
/// <param name="trade">交易记录</param>
/// <param name="clientCode">客户代码</param>
/// <returns>确认书编号</returns>
public static string Generate(
YLContext dbContext,
trade trade,
string clientCode)
{
var tradeDate = trade.TradeDate ?? DateTime.MinValue;
lock (_syncLock)
{
// 幂等:如果已生成过确认书编号,直接返回
var existingForTrade = dbContext.trade_contract_r
.Where(r => r.TradeId == trade.id && r.Type == ContractTypeEnum.Trade && r.IsValid)
.Select(r => r.ContractCode)
.FirstOrDefault();
if (!string.IsNullOrWhiteSpace(existingForTrade))
{
return existingForTrade;
}
// 通过当天同客户同类交易的 id 排序确定序号(幂等),每个客户独立递增
var tradeDateStart = tradeDate.Date;
var sameDayTradesCount = dbContext.trade
.Count(t => t.TradeDate >= tradeDateStart
&& t.TradeDate < tradeDateStart.AddDays(1)
&& t.ClientId == trade.ClientId
&& t.OpponentRole == trade.OpponentRole
&& t.id < trade.id);
var sequenceNo = sameDayTradesCount + 1;
return BuildTradeNumber(trade, clientCode, sequenceNo);
}
}
/// <summary>
/// 将数字转换为字母序列(1=A, 2=B, 26=Z, 27=AA, 28=AB...
/// </summary>
private static string NumberToLetter(int number)
{
if (number <= 0)
{
return "A";
}
string result = "";
while (number > 0)
{
number--; // 调整为从0开始
result = (char)('A' + (number % 26)) + result;
number /= 26;
}
return result;
}
}
}