using YLErp.BLL; using YLErp.Configuration; using YLErp.DBModels; using YLErp.DBModels.Consts; using YLErp.DBModels.Enums; namespace YLErp.Modules.TradeModule.DocGenerateModule { /// /// 国联交易确认书编号生成器 /// 规则: /// - 代客交易(簿记分类为"代客"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔代客交易(数字递增)}-{标的代码},从01开始 /// 例:GLMS-IS-20260424-01-250210IB /// - 融券交易(簿记分类为"融券"):GLMS-{对手方代码}-{成交日期(yyyyMMdd)}-FICC-{当日第N笔融券交易使用字母递增}-{标的代码},从A开始 /// 例:GLMS-IS-20260424-A-250210IB /// 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); } /// /// 交易首次入库后按国联交易确认书编号规则回写交易编号。 /// /// 是否生成了交易编号 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; } /// /// 按国联确认书编号规则拼装编号(纯函数,不依赖数据库,便于单测)。 /// 格式:GLMS-{clientCode}-{成交日期(yyyyMMdd)}-FICC-{序号}-{标的代码(去点)} /// - 代客交易:序号用两位数字,从 01 开始 /// - 融券交易:序号用字母递增,从 A 开始 /// 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 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()) { 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; } /// /// 生成国贸交易确认书编号 /// /// 数据库上下文 /// 交易记录 /// 客户代码 /// 确认书编号 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; } /// /// 将数字转换为字母序列(1=A, 2=B, 26=Z, 27=AA, 28=AB...) /// 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; } } }