using SqlSugar; namespace Admin.NET.Plugin.AiDOP.DataPlatform.Sequence; /// /// 在 165 dopdemorq 上原子分配单号(不调用存储过程)。 /// 通过 mdp_source=DOPDEMORQ_SQLSERVER 取连接;源须启用且具备写权限。 /// public sealed class NbrSequenceService : INbrSequenceService, ITransient { public const string DefaultSourceCode = "DOPDEMORQ_SQLSERVER"; private const int MaxLocateRetries = 3; private readonly MdpSourceScopeFactory _scopeFactory; public NbrSequenceService(MdpSourceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task> AllocateAsync( string nbrType, int increment = 1, string domain = "8010", string userNo = "", CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(nbrType)) throw new ArgumentException("nbrType 不能为空", nameof(nbrType)); if (increment <= 0) throw new ArgumentOutOfRangeException(nameof(increment), "increment 必须 > 0"); if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentException("domain 不能为空", nameof(domain)); var db = await _scopeFactory.GetScopeAsync(DefaultSourceCode, ct); var now = DateTime.Now; var ctrl = await db.Ado.SqlQuerySingleAsync( @"SELECT Domain, NbrType, ISNULL(NbrPre1,'') AS NbrPre1, ISNULL(NbrPre2,'') AS NbrPre2, ISNULL(NbrPre3,'') AS NbrPre3, ISNULL(DateType,'') AS DateType, IniValue, NextValue AS CtrlNextValue, MinValue, MaxValue, AllowReset, ResetValue, AllowSkip FROM NbrControl WHERE Domain=@domain AND NbrType=@nbrType", new SugarParameter("@domain", domain), new SugarParameter("@nbrType", nbrType)); if (ctrl == null) throw new InvalidOperationException($"NbrControl 未配置:Domain={domain}, NbrType={nbrType}"); var dateType = ctrl.DateType?.Trim() ?? ""; string normalizedFmt = ""; if (dateType.Length > 0) { var fmtRow = await db.Ado.SqlQuerySingleAsync( @"SELECT DateFormat FROM NbrDateFormat WHERE Domain=@domain AND DateType=@dateType", new SugarParameter("@domain", domain), new SugarParameter("@dateType", dateType)); // DateType 大小写不一致时(如 wo=yyyymmdd)再忽略大小写查一次 if (fmtRow == null) { var all = await db.Ado.SqlQueryAsync( "SELECT DateType, DateFormat FROM NbrDateFormat WHERE Domain=@domain", new SugarParameter("@domain", domain)); fmtRow = all.FirstOrDefault(x => string.Equals(x.DateType, dateType, StringComparison.OrdinalIgnoreCase)); } normalizedFmt = NbrSequenceFormatter.NormalizeDateFormat(fmtRow?.DateFormat); } var (nbrDate, nbrDateStr) = NbrSequenceFormatter.ResolvePeriod(normalizedFmt, now); var padWidth = NbrSequenceFormatter.ResolvePadWidth(ctrl.MaxValue); var pre1 = ctrl.NbrPre1 ?? ""; var pre2 = ctrl.NbrPre2 ?? ""; var pre3 = ctrl.NbrPre3 ?? ""; for (var attempt = 0; attempt < MaxLocateRetries; attempt++) { ct.ThrowIfCancellationRequested(); var day = await LocateDayInfoAsync(db, domain, nbrType, normalizedFmt, nbrDate); if (day == null) { await TryInsertDayInfoAsync(db, ctrl, domain, nbrType, nbrDate, userNo); continue; } // 溢出预检:当前最后发出号 + 控制表 NextValue(余量)> MaxValue 时走重置/失败 // 真正步进量是 increment;此处用 day.NextValue + increment 判断是否越界 var projected = day.NextValue + increment; if (projected > ctrl.MaxValue) { if (ctrl.AllowReset) { var resetTo = ctrl.ResetValue > 0 ? ctrl.ResetValue : ctrl.IniValue; // 重置后首号 = resetTo,发出后 NextValue = resetTo - 1 + increment // 用条件 UPDATE 确保仍在溢出态 var resetAffected = await db.Ado.ExecuteCommandAsync( @"UPDATE NbrDayInfo WITH (ROWLOCK) SET NextValue = @newNext, UpdateTime=GETDATE(), UpdateUser=@user WHERE RecID=@recId AND NextValue=@oldNext", new SugarParameter("@newNext", resetTo - 1 + increment), new SugarParameter("@user", userNo ?? ""), new SugarParameter("@recId", day.RecID), new SugarParameter("@oldNext", day.NextValue)); if (resetAffected == 0) continue; return NbrSequenceFormatter.BuildRange(pre1, pre2, pre3, nbrDateStr, (long)resetTo, increment, padWidth); } throw new InvalidOperationException( $"创建单号失败,单号超过最大数,请重置:Domain={domain}, NbrType={nbrType}, NextValue={day.NextValue}, MaxValue={ctrl.MaxValue}"); } // 原子递增:OUTPUT deleted.NextValue = 本次号段起点(最后已发) var lastIssued = await db.Ado.SqlQuerySingleAsync( @"UPDATE NbrDayInfo WITH (ROWLOCK) SET NextValue = NextValue + @increment, UpdateTime = GETDATE(), UpdateUser = @user OUTPUT deleted.NextValue WHERE RecID = @recId", new SugarParameter("@increment", increment), new SugarParameter("@user", userNo ?? ""), new SugarParameter("@recId", day.RecID)); if (lastIssued == null) continue; // 行被并发删/改,重试 var first = (long)lastIssued.Value + 1; // 格式化前再校验宽度(陷阱 #6) return NbrSequenceFormatter.BuildRange(pre1, pre2, pre3, nbrDateStr, first, increment, padWidth); } throw new InvalidOperationException($"单号分配失败:无法定位/更新 NbrDayInfo(Domain={domain}, NbrType={nbrType})"); } private static async Task LocateDayInfoAsync( ISqlSugarClient db, string domain, string nbrType, string normalizedFmt, DateTime nbrDate) { if (normalizedFmt.Length == 0) { return await db.Ado.SqlQuerySingleAsync( @"SELECT TOP 1 RecID, Domain, NbrType, Today, NextValue, MaxValue, IniValue FROM NbrDayInfo WHERE Domain=@domain AND NbrType=@nbrType ORDER BY RecID", new SugarParameter("@domain", domain), new SugarParameter("@nbrType", nbrType)); } var dateKey = nbrDate.ToString("yyyy-MM-dd"); return await db.Ado.SqlQuerySingleAsync( @"SELECT TOP 1 RecID, Domain, NbrType, Today, NextValue, MaxValue, IniValue FROM NbrDayInfo WHERE Domain=@domain AND NbrType=@nbrType AND CONVERT(varchar(10), Today, 23) = @nbrDate ORDER BY RecID", new SugarParameter("@domain", domain), new SugarParameter("@nbrType", nbrType), new SugarParameter("@nbrDate", dateKey)); } private static async Task TryInsertDayInfoAsync( ISqlSugarClient db, NbrControlRow ctrl, string domain, string nbrType, DateTime nbrDate, string userNo) { // 从 NbrControl 整行复制;NextValue 先置 IniValue-1,首次原子递增后发出 IniValue // 并发下可能双插,靠后续重新定位收敛(165 不可加唯一索引) var dateKey = nbrDate.ToString("yyyy-MM-dd"); await db.Ado.ExecuteCommandAsync( @"IF NOT EXISTS ( SELECT 1 FROM NbrDayInfo WHERE Domain=@domain AND NbrType=@nbrType AND (@hasDate=0 OR CONVERT(varchar(10),Today,23)=@nbrDate) ) INSERT INTO NbrDayInfo (Domain, NbrType, Today, NextValue, MaxValue, IniValue, MinValue, CreateTime, CreateUser, UpdateTime, UpdateUser) SELECT Domain, NbrType, @today, IniValue - 1, MaxValue, IniValue, MinValue, GETDATE(), @user, GETDATE(), @user FROM NbrControl WHERE Domain=@domain AND NbrType=@nbrType", new SugarParameter("@domain", domain), new SugarParameter("@nbrType", nbrType), new SugarParameter("@hasDate", string.IsNullOrEmpty(ctrl.DateType) ? 0 : 1), new SugarParameter("@nbrDate", dateKey), new SugarParameter("@today", nbrDate.Date), new SugarParameter("@user", userNo ?? "")); } private sealed class NbrControlRow { public string Domain { get; set; } = ""; public string NbrType { get; set; } = ""; public string NbrPre1 { get; set; } = ""; public string NbrPre2 { get; set; } = ""; public string NbrPre3 { get; set; } = ""; public string DateType { get; set; } = ""; public decimal IniValue { get; set; } public decimal CtrlNextValue { get; set; } public decimal MinValue { get; set; } public decimal MaxValue { get; set; } public bool AllowReset { get; set; } public decimal ResetValue { get; set; } public bool AllowSkip { get; set; } } private sealed class NbrDateFormatRow { public string? DateType { get; set; } public string? DateFormat { get; set; } } private sealed class NbrDayInfoRow { public int RecID { get; set; } public string Domain { get; set; } = ""; public string NbrType { get; set; } = ""; public DateTime? Today { get; set; } public decimal NextValue { get; set; } public decimal MaxValue { get; set; } public decimal IniValue { get; set; } } }