NbrSequenceService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. using SqlSugar;
  2. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Sequence;
  3. /// <summary>
  4. /// 在 165 dopdemorq 上原子分配单号(不调用存储过程)。
  5. /// 通过 mdp_source=DOPDEMORQ_SQLSERVER 取连接;源须启用且具备写权限。
  6. /// </summary>
  7. public sealed class NbrSequenceService : INbrSequenceService, ITransient
  8. {
  9. public const string DefaultSourceCode = "DOPDEMORQ_SQLSERVER";
  10. private const int MaxLocateRetries = 3;
  11. private readonly MdpSourceScopeFactory _scopeFactory;
  12. public NbrSequenceService(MdpSourceScopeFactory scopeFactory)
  13. {
  14. _scopeFactory = scopeFactory;
  15. }
  16. public async Task<IReadOnlyList<string>> AllocateAsync(
  17. string nbrType,
  18. int increment = 1,
  19. string domain = "8010",
  20. string userNo = "",
  21. CancellationToken ct = default)
  22. {
  23. if (string.IsNullOrWhiteSpace(nbrType))
  24. throw new ArgumentException("nbrType 不能为空", nameof(nbrType));
  25. if (increment <= 0)
  26. throw new ArgumentOutOfRangeException(nameof(increment), "increment 必须 > 0");
  27. if (string.IsNullOrWhiteSpace(domain))
  28. throw new ArgumentException("domain 不能为空", nameof(domain));
  29. var db = await _scopeFactory.GetScopeAsync(DefaultSourceCode, ct);
  30. var now = DateTime.Now;
  31. var ctrl = await db.Ado.SqlQuerySingleAsync<NbrControlRow>(
  32. @"SELECT Domain, NbrType, ISNULL(NbrPre1,'') AS NbrPre1, ISNULL(NbrPre2,'') AS NbrPre2, ISNULL(NbrPre3,'') AS NbrPre3,
  33. ISNULL(DateType,'') AS DateType, IniValue, NextValue AS CtrlNextValue, MinValue, MaxValue,
  34. AllowReset, ResetValue, AllowSkip
  35. FROM NbrControl
  36. WHERE Domain=@domain AND NbrType=@nbrType",
  37. new SugarParameter("@domain", domain),
  38. new SugarParameter("@nbrType", nbrType));
  39. if (ctrl == null)
  40. throw new InvalidOperationException($"NbrControl 未配置:Domain={domain}, NbrType={nbrType}");
  41. var dateType = ctrl.DateType?.Trim() ?? "";
  42. string normalizedFmt = "";
  43. if (dateType.Length > 0)
  44. {
  45. var fmtRow = await db.Ado.SqlQuerySingleAsync<NbrDateFormatRow>(
  46. @"SELECT DateFormat FROM NbrDateFormat
  47. WHERE Domain=@domain AND DateType=@dateType",
  48. new SugarParameter("@domain", domain),
  49. new SugarParameter("@dateType", dateType));
  50. // DateType 大小写不一致时(如 wo=yyyymmdd)再忽略大小写查一次
  51. if (fmtRow == null)
  52. {
  53. var all = await db.Ado.SqlQueryAsync<NbrDateFormatRow>(
  54. "SELECT DateType, DateFormat FROM NbrDateFormat WHERE Domain=@domain",
  55. new SugarParameter("@domain", domain));
  56. fmtRow = all.FirstOrDefault(x =>
  57. string.Equals(x.DateType, dateType, StringComparison.OrdinalIgnoreCase));
  58. }
  59. normalizedFmt = NbrSequenceFormatter.NormalizeDateFormat(fmtRow?.DateFormat);
  60. }
  61. var (nbrDate, nbrDateStr) = NbrSequenceFormatter.ResolvePeriod(normalizedFmt, now);
  62. var padWidth = NbrSequenceFormatter.ResolvePadWidth(ctrl.MaxValue);
  63. var pre1 = ctrl.NbrPre1 ?? "";
  64. var pre2 = ctrl.NbrPre2 ?? "";
  65. var pre3 = ctrl.NbrPre3 ?? "";
  66. for (var attempt = 0; attempt < MaxLocateRetries; attempt++)
  67. {
  68. ct.ThrowIfCancellationRequested();
  69. var day = await LocateDayInfoAsync(db, domain, nbrType, normalizedFmt, nbrDate);
  70. if (day == null)
  71. {
  72. await TryInsertDayInfoAsync(db, ctrl, domain, nbrType, nbrDate, userNo);
  73. continue;
  74. }
  75. // 溢出预检:当前最后发出号 + 控制表 NextValue(余量)> MaxValue 时走重置/失败
  76. // 真正步进量是 increment;此处用 day.NextValue + increment 判断是否越界
  77. var projected = day.NextValue + increment;
  78. if (projected > ctrl.MaxValue)
  79. {
  80. if (ctrl.AllowReset)
  81. {
  82. var resetTo = ctrl.ResetValue > 0 ? ctrl.ResetValue : ctrl.IniValue;
  83. // 重置后首号 = resetTo,发出后 NextValue = resetTo - 1 + increment
  84. // 用条件 UPDATE 确保仍在溢出态
  85. var resetAffected = await db.Ado.ExecuteCommandAsync(
  86. @"UPDATE NbrDayInfo WITH (ROWLOCK)
  87. SET NextValue = @newNext, UpdateTime=GETDATE(), UpdateUser=@user
  88. WHERE RecID=@recId AND NextValue=@oldNext",
  89. new SugarParameter("@newNext", resetTo - 1 + increment),
  90. new SugarParameter("@user", userNo ?? ""),
  91. new SugarParameter("@recId", day.RecID),
  92. new SugarParameter("@oldNext", day.NextValue));
  93. if (resetAffected == 0) continue;
  94. return NbrSequenceFormatter.BuildRange(pre1, pre2, pre3, nbrDateStr, (long)resetTo, increment, padWidth);
  95. }
  96. throw new InvalidOperationException(
  97. $"创建单号失败,单号超过最大数,请重置:Domain={domain}, NbrType={nbrType}, NextValue={day.NextValue}, MaxValue={ctrl.MaxValue}");
  98. }
  99. // 原子递增:OUTPUT deleted.NextValue = 本次号段起点(最后已发)
  100. var lastIssued = await db.Ado.SqlQuerySingleAsync<decimal?>(
  101. @"UPDATE NbrDayInfo WITH (ROWLOCK)
  102. SET NextValue = NextValue + @increment,
  103. UpdateTime = GETDATE(),
  104. UpdateUser = @user
  105. OUTPUT deleted.NextValue
  106. WHERE RecID = @recId",
  107. new SugarParameter("@increment", increment),
  108. new SugarParameter("@user", userNo ?? ""),
  109. new SugarParameter("@recId", day.RecID));
  110. if (lastIssued == null)
  111. continue; // 行被并发删/改,重试
  112. var first = (long)lastIssued.Value + 1;
  113. // 格式化前再校验宽度(陷阱 #6)
  114. return NbrSequenceFormatter.BuildRange(pre1, pre2, pre3, nbrDateStr, first, increment, padWidth);
  115. }
  116. throw new InvalidOperationException($"单号分配失败:无法定位/更新 NbrDayInfo(Domain={domain}, NbrType={nbrType})");
  117. }
  118. private static async Task<NbrDayInfoRow?> LocateDayInfoAsync(
  119. ISqlSugarClient db, string domain, string nbrType, string normalizedFmt, DateTime nbrDate)
  120. {
  121. if (normalizedFmt.Length == 0)
  122. {
  123. return await db.Ado.SqlQuerySingleAsync<NbrDayInfoRow>(
  124. @"SELECT TOP 1 RecID, Domain, NbrType, Today, NextValue, MaxValue, IniValue
  125. FROM NbrDayInfo
  126. WHERE Domain=@domain AND NbrType=@nbrType
  127. ORDER BY RecID",
  128. new SugarParameter("@domain", domain),
  129. new SugarParameter("@nbrType", nbrType));
  130. }
  131. var dateKey = nbrDate.ToString("yyyy-MM-dd");
  132. return await db.Ado.SqlQuerySingleAsync<NbrDayInfoRow>(
  133. @"SELECT TOP 1 RecID, Domain, NbrType, Today, NextValue, MaxValue, IniValue
  134. FROM NbrDayInfo
  135. WHERE Domain=@domain AND NbrType=@nbrType
  136. AND CONVERT(varchar(10), Today, 23) = @nbrDate
  137. ORDER BY RecID",
  138. new SugarParameter("@domain", domain),
  139. new SugarParameter("@nbrType", nbrType),
  140. new SugarParameter("@nbrDate", dateKey));
  141. }
  142. private static async Task TryInsertDayInfoAsync(
  143. ISqlSugarClient db, NbrControlRow ctrl, string domain, string nbrType, DateTime nbrDate, string userNo)
  144. {
  145. // 从 NbrControl 整行复制;NextValue 先置 IniValue-1,首次原子递增后发出 IniValue
  146. // 并发下可能双插,靠后续重新定位收敛(165 不可加唯一索引)
  147. var dateKey = nbrDate.ToString("yyyy-MM-dd");
  148. await db.Ado.ExecuteCommandAsync(
  149. @"IF NOT EXISTS (
  150. SELECT 1 FROM NbrDayInfo
  151. WHERE Domain=@domain AND NbrType=@nbrType
  152. AND (@hasDate=0 OR CONVERT(varchar(10),Today,23)=@nbrDate)
  153. )
  154. INSERT INTO NbrDayInfo
  155. (Domain, NbrType, Today, NextValue, MaxValue, IniValue, MinValue,
  156. CreateTime, CreateUser, UpdateTime, UpdateUser)
  157. SELECT Domain, NbrType, @today, IniValue - 1, MaxValue, IniValue, MinValue,
  158. GETDATE(), @user, GETDATE(), @user
  159. FROM NbrControl
  160. WHERE Domain=@domain AND NbrType=@nbrType",
  161. new SugarParameter("@domain", domain),
  162. new SugarParameter("@nbrType", nbrType),
  163. new SugarParameter("@hasDate", string.IsNullOrEmpty(ctrl.DateType) ? 0 : 1),
  164. new SugarParameter("@nbrDate", dateKey),
  165. new SugarParameter("@today", nbrDate.Date),
  166. new SugarParameter("@user", userNo ?? ""));
  167. }
  168. private sealed class NbrControlRow
  169. {
  170. public string Domain { get; set; } = "";
  171. public string NbrType { get; set; } = "";
  172. public string NbrPre1 { get; set; } = "";
  173. public string NbrPre2 { get; set; } = "";
  174. public string NbrPre3 { get; set; } = "";
  175. public string DateType { get; set; } = "";
  176. public decimal IniValue { get; set; }
  177. public decimal CtrlNextValue { get; set; }
  178. public decimal MinValue { get; set; }
  179. public decimal MaxValue { get; set; }
  180. public bool AllowReset { get; set; }
  181. public decimal ResetValue { get; set; }
  182. public bool AllowSkip { get; set; }
  183. }
  184. private sealed class NbrDateFormatRow
  185. {
  186. public string? DateType { get; set; }
  187. public string? DateFormat { get; set; }
  188. }
  189. private sealed class NbrDayInfoRow
  190. {
  191. public int RecID { get; set; }
  192. public string Domain { get; set; } = "";
  193. public string NbrType { get; set; } = "";
  194. public DateTime? Today { get; set; }
  195. public decimal NextValue { get; set; }
  196. public decimal MaxValue { get; set; }
  197. public decimal IniValue { get; set; }
  198. }
  199. }