#EQD-6605 国联民生-确认书编号规则调整(不基于甲方乙方判定)

feat: 国联民生收益互换按簿记分类生成交易编号

簿记账户新增“簿记分类”字段,支持代客、融券,新增账户默认代客,历史账户统一初始化为代客
簿记账户分类设为必填,列表、编辑及详情页面同步展示
修改已被交易引用的簿记账户分类时增加二次确认,并提示关联交易数量
国联民生收益互换交易编号不再根据甲乙方判断,改为根据簿记账户分类生成
代客使用数字序号,融券使用字母序号,分别从已有同客户同日编号的最大序号继续递增
保留手工交易编号优先逻辑,标准格式的手工编号参与后续最大序号计算
生成候选编号后检查完整交易编号是否已存在,存在则继续递增
新增簿记分类编号格式、最大序号计算及数字字母序列隔离专项单测
This commit is contained in:
tengyufan
2026-08-05 15:55:01 +08:00
parent 74121a89ed
commit 864a29d6a8
12 changed files with 310 additions and 32 deletions
@@ -7,11 +7,11 @@ using YLErp.DBModels.Enums;
namespace YLErp.Modules.TradeModule.DocGenerateModule
{
/// <summary>
/// 国交易确认书编号生成器
/// 国交易确认书编号生成器
/// 规则:
/// - 客交易(OpponentRole为"甲方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔客交易(数字递增)}-{标的代码},从01开始
/// - 客交易(簿记分类为"代客"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔客交易(数字递增)}-{标的代码},从01开始
/// 例:GLMS-IS-20260424-01-250210IB
/// - 非对客交易(OpponentRole为"乙方"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔非对客交易使用字母递增}-{标的代码},从A开始
/// - 融券交易(簿记分类为"融券"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔融券交易使用字母递增}-{标的代码},从A开始
/// 例:GLMS-IS-20260424-A-250210IB
/// </summary>
public class GuolianContractNoGenerator
@@ -78,18 +78,69 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
/// <summary>
/// 按国联确认书编号规则拼装编号(纯函数,不依赖数据库,便于单测)。
/// 格式:GLMS-{clientCode}-{成交日期(yyyyMMdd)}-FICC-{序号}-{标的代码(去点)}
/// - 客交易(甲方):序号用两位数字,从 01 开始
/// - 非对客交易(乙方):序号用字母递增,从 A 开始
/// - 客交易:序号用两位数字,从 01 开始
/// - 融券交易:序号用字母递增,从 A 开始
/// </summary>
public static string BuildTradeNumber(trade trade, string clientCode, int sequenceNo)
public static string BuildTradeNumber(trade trade, string clientCode, string bookCategory, int sequenceNo)
{
var isClientTrade = trade.OpponentRole == "甲方";
var isAgencyTrade = bookCategory == AssetUnit.BookCategoryAgency;
var underlyingCode = (trade.UnderlyingCode ?? "").Replace(".", ""); // 去掉标的代码中的点号
var tradeDateStr = (trade.TradeDate ?? DateTime.MinValue).ToString("yyyyMMdd");
var seq = isClientTrade ? sequenceNo.ToString("D2") : NumberToLetter(sequenceNo);
var seq = isAgencyTrade ? sequenceNo.ToString("D2") : NumberToLetter(sequenceNo);
return $"GLMS-{clientCode}-{tradeDateStr}-FICC-{seq}-{underlyingCode}";
}
public static int GetNextSequenceNo(
IEnumerable<string> tradeNumbers,
string clientCode,
DateTime tradeDate,
string bookCategory)
{
if (bookCategory != AssetUnit.BookCategoryAgency &&
bookCategory != AssetUnit.BookCategorySecuritiesLending)
{
throw new ArgumentException("簿记分类无效", nameof(bookCategory));
}
var prefix = $"GLMS-{clientCode}-{tradeDate:yyyyMMdd}-FICC-";
var isAgencyTrade = bookCategory == AssetUnit.BookCategoryAgency;
var maxSequenceNo = 0;
foreach (var tradeNumber in tradeNumbers ?? Enumerable.Empty<string>())
{
if (string.IsNullOrWhiteSpace(tradeNumber) ||
!tradeNumber.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var sequenceEndIndex = tradeNumber.IndexOf('-', prefix.Length);
if (sequenceEndIndex <= prefix.Length)
{
continue;
}
var sequenceText = tradeNumber.Substring(
prefix.Length,
sequenceEndIndex - prefix.Length);
var sequenceNo = isAgencyTrade
? ParseNumericSequence(sequenceText)
: LetterToNumber(sequenceText);
if (sequenceNo.HasValue && sequenceNo.Value > maxSequenceNo)
{
maxSequenceNo = sequenceNo.Value;
}
}
if (maxSequenceNo == int.MaxValue)
{
throw new InvalidOperationException("国联收益互换交易编号序号已超出支持范围");
}
return maxSequenceNo + 1;
}
/// <summary>
/// 生成国贸交易确认书编号
/// </summary>
@@ -103,6 +154,16 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
string clientCode)
{
var tradeDate = trade.TradeDate ?? DateTime.MinValue;
var bookCategory = dbContext.assetunit
.Where(a => a.id == trade.AssetId)
.Select(a => a.BookCategory)
.FirstOrDefault();
if (bookCategory != AssetUnit.BookCategoryAgency &&
bookCategory != AssetUnit.BookCategorySecuritiesLending)
{
throw new ServiceException("簿记账户未维护有效的簿记分类");
}
lock (_syncLock)
{
@@ -117,20 +178,77 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule
return existingForTrade;
}
// 通过当天同客户同类交易的 id 排序确定序号(幂等),每个客户独立递增
// 编号中的数字/字母序列记录生成时的分类,不受簿记账户后续分类调整影响。
var tradeDateStart = tradeDate.Date;
var sameDayTradesCount = dbContext.trade
.Count(t => t.TradeDate >= tradeDateStart
var sameDayTradeNumbers = dbContext.trade
.Where(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;
&& t.id != trade.id
&& t.TradeNumber != null
&& t.TradeNumber != "")
.Select(t => t.TradeNumber)
.ToArray();
var sequenceNo = GetNextSequenceNo(
sameDayTradeNumbers,
clientCode,
tradeDate,
bookCategory);
return BuildTradeNumber(trade, clientCode, sequenceNo);
while (true)
{
var candidate = BuildTradeNumber(trade, clientCode, bookCategory, sequenceNo);
var isExists = dbContext.trade.Any(t => t.id != trade.id && t.TradeNumber == candidate);
if (!isExists)
{
return candidate;
}
if (sequenceNo == int.MaxValue)
{
throw new InvalidOperationException("国联收益互换交易编号序号已超出支持范围");
}
sequenceNo++;
}
}
}
private static int? ParseNumericSequence(string sequence)
{
return sequence.All(char.IsDigit) && int.TryParse(sequence, out var number) && number > 0
? number
: null;
}
private static int? LetterToNumber(string sequence)
{
if (string.IsNullOrWhiteSpace(sequence))
{
return null;
}
var number = 0;
foreach (var value in sequence)
{
var letter = char.ToUpperInvariant(value);
if (letter < 'A' || letter > 'Z')
{
return null;
}
var letterValue = letter - 'A' + 1;
if (number > (int.MaxValue - letterValue) / 26)
{
return null;
}
number = number * 26 + letterValue;
}
return number;
}
/// <summary>
/// 将数字转换为字母序列(1=A, 2=B, 26=Z, 27=AA, 28=AB...
/// </summary>
@@ -450,7 +450,7 @@ namespace YLErp.Modules.TradeModule.OrderModule
if (_assetUnit == null)
{
_assetUnit = DbContext.assetunit.Where(n => n.id == req.AssetId)
.Select(n => new AssetUnitDto { id = n.id, Name = n.Name, TraderIds = n.TraderIds, UserGroup = n.UserGroup }).FirstOrDefault();
.Select(n => new AssetUnitDto { id = n.id, Name = n.Name, BookCategory = n.BookCategory, TraderIds = n.TraderIds, UserGroup = n.UserGroup }).FirstOrDefault();
if (_assetUnit == null)
{
throw new ServiceException("簿记账户不存在");
@@ -1576,4 +1576,4 @@ namespace YLErp.Modules.TradeModule.OrderModule
}
}
}
}
}