Files
zszq-trs/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianContractNoGenerator.cs
T
tengyufan 864a29d6a8 #EQD-6605 国联民生-确认书编号规则调整(不基于甲方乙方判定)
feat: 国联民生收益互换按簿记分类生成交易编号

簿记账户新增“簿记分类”字段,支持代客、融券,新增账户默认代客,历史账户统一初始化为代客
簿记账户分类设为必填,列表、编辑及详情页面同步展示
修改已被交易引用的簿记账户分类时增加二次确认,并提示关联交易数量
国联民生收益互换交易编号不再根据甲乙方判断,改为根据簿记账户分类生成
代客使用数字序号,融券使用字母序号,分别从已有同客户同日编号的最大序号继续递增
保留手工交易编号优先逻辑,标准格式的手工编号参与后续最大序号计算
生成候选编号后检查完整交易编号是否已存在,存在则继续递增
新增簿记分类编号格式、最大序号计算及数字字母序列隔离专项单测
2026-08-05 15:55:01 +08:00

273 lines
10 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>
/// 国联交易确认书编号生成器
/// 规则:
/// - 代客交易(簿记分类为"代客"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔代客交易(数字递增)}-{标的代码},从01开始
/// 例:GLMS-IS-20260424-01-250210IB
/// - 融券交易(簿记分类为"融券"):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, string bookCategory, int sequenceNo)
{
var isAgencyTrade = bookCategory == AssetUnit.BookCategoryAgency;
var underlyingCode = (trade.UnderlyingCode ?? "").Replace(".", ""); // 去掉标的代码中的点号
var tradeDateStr = (trade.TradeDate ?? DateTime.MinValue).ToString("yyyyMMdd");
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>
/// <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;
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)
{
// 幂等:如果已生成过确认书编号,直接返回
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;
}
// 编号中的数字/字母序列记录生成时的分类,不受簿记账户后续分类调整影响。
var tradeDateStart = tradeDate.Date;
var sameDayTradeNumbers = dbContext.trade
.Where(t => t.TradeDate >= tradeDateStart
&& t.TradeDate < tradeDateStart.AddDays(1)
&& t.ClientId == trade.ClientId
&& t.id != trade.id
&& t.TradeNumber != null
&& t.TradeNumber != "")
.Select(t => t.TradeNumber)
.ToArray();
var sequenceNo = GetNextSequenceNo(
sameDayTradeNumbers,
clientCode,
tradeDate,
bookCategory);
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>
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;
}
}
}