NumberRuleService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. namespace Admin.NET.Plugin.AiDOP.Supply;
  3. /// <summary>
  4. /// 编号规则服务。第一阶段兼容现有 NbrControl 规则,替代业务代码直接调用编号存储过程。
  5. /// </summary>
  6. public class NumberRuleService : ITransient
  7. {
  8. private readonly ISqlSugarClient _db;
  9. public NumberRuleService(ISqlSugarClient db)
  10. {
  11. _db = db;
  12. }
  13. public async Task<IReadOnlyList<string>> NextBatchAsync(string ruleCode, string domain, int count, string operatorNo)
  14. {
  15. try
  16. {
  17. _db.Ado.BeginTran();
  18. var numbers = await NextBatchInCurrentTransactionAsync(ruleCode, domain, count, operatorNo);
  19. _db.Ado.CommitTran();
  20. return numbers;
  21. }
  22. catch
  23. {
  24. _db.Ado.RollbackTran();
  25. throw;
  26. }
  27. }
  28. public async Task<IReadOnlyList<string>> NextBatchInCurrentTransactionAsync(string ruleCode, string domain, int count, string operatorNo)
  29. {
  30. if (string.IsNullOrWhiteSpace(ruleCode)) throw Oops.Oh("编号规则不能为空。");
  31. if (count <= 0) throw Oops.Oh("编号数量必须大于 0。");
  32. var normalizedRuleCode = ruleCode.Trim();
  33. var normalizedDomain = domain?.Trim() ?? string.Empty;
  34. var now = DateTime.Now;
  35. // 可信租户硬解析:无 token / 超管未选租户一律抛错;绝不回退固定默认租户、绝不读前端 tenantId。
  36. // 租户过滤在取号规则定位上强制无条件生效,杜绝跨租户读到他租户规则或落到默认规则。
  37. var tenantId = AdoS0TenantScope.ResolveRequiredOrThrow();
  38. const string tenantWhere = "\n AND tenant_id = @TenantId";
  39. var rulePars = new List<SugarParameter>
  40. {
  41. new("@RuleCode", normalizedRuleCode),
  42. new("@Domain", normalizedDomain),
  43. new("@TenantId", tenantId)
  44. };
  45. var rule = (await _db.Ado.SqlQueryAsync<NbrControlRuleRow>(
  46. $"""
  47. SELECT
  48. RecID AS Id,
  49. NbrType,
  50. NbrPre1,
  51. NbrPre2,
  52. NbrPre3,
  53. DateType,
  54. IsDateType,
  55. Description,
  56. IniValue,
  57. MinValue,
  58. `MaxValue` AS `MaxValue`,
  59. ResetValue,
  60. AllowReset,
  61. AllowSkip,
  62. AllowManual,
  63. IsActive,
  64. IsConfirm,
  65. Domain,
  66. tenant_id AS TenantId
  67. FROM NbrControl
  68. WHERE LOWER(TRIM(IFNULL(NbrType,''))) = LOWER(@RuleCode)
  69. AND (
  70. IFNULL(TRIM(domain_code),'') = @Domain
  71. OR IFNULL(TRIM(Domain),'') = @Domain
  72. OR IFNULL(TRIM(domain_code),'') = ''
  73. OR IFNULL(TRIM(Domain),'') = ''
  74. )
  75. AND IFNULL(IsActive, 1) = 1{tenantWhere}
  76. ORDER BY
  77. CASE
  78. WHEN IFNULL(TRIM(domain_code),'') = @Domain THEN 0
  79. WHEN IFNULL(TRIM(Domain),'') = @Domain THEN 1
  80. ELSE 2
  81. END,
  82. RecID
  83. LIMIT 1
  84. FOR UPDATE
  85. """,
  86. rulePars)).FirstOrDefault();
  87. if (rule == null)
  88. throw Oops.Oh($"未找到编号规则:{normalizedRuleCode}");
  89. var sequenceDate = NumberRuleFormatter.ResolveSequenceDate(rule.DateType, rule.IsDateType, now);
  90. var dayInfo = await GetDayInfoForUpdateAsync(rule, normalizedRuleCode, normalizedDomain, sequenceDate, tenantId);
  91. var firstSerial = ResolveFirstSerial(rule, dayInfo);
  92. var lastSerial = firstSerial + count - 1;
  93. if (rule.MaxValue.HasValue && lastSerial > rule.MaxValue.Value)
  94. {
  95. if (rule.AllowReset == true)
  96. {
  97. firstSerial = (rule.ResetValue ?? 0) + 1;
  98. lastSerial = firstSerial + count - 1;
  99. }
  100. if (lastSerial > rule.MaxValue.Value)
  101. throw Oops.Oh($"编号规则 {normalizedRuleCode} 已超过最大值 {rule.MaxValue.Value}。");
  102. }
  103. var numbers = Enumerable.Range(firstSerial, count)
  104. .Select(serial => NumberRuleFormatter.BuildNumber(
  105. rule.NbrPre1,
  106. rule.NbrPre2,
  107. rule.NbrPre3,
  108. rule.DateType,
  109. rule.IsDateType,
  110. serial,
  111. rule.MaxValue,
  112. now))
  113. .ToList();
  114. if (dayInfo == null)
  115. await InsertDayInfoAsync(rule, sequenceDate, lastSerial, operatorNo, now);
  116. else
  117. await UpdateDayInfoAsync(dayInfo.Id, lastSerial, operatorNo, now);
  118. return numbers;
  119. }
  120. private async Task<NbrDayInfoRow?> GetDayInfoForUpdateAsync(NbrControlRuleRow rule, string ruleCode, string domain, DateTime? sequenceDate, long tenantId)
  121. {
  122. var dateFilter = sequenceDate.HasValue ? "AND DATE(Today) = DATE(@Today)" : string.Empty;
  123. // 日流水行同样按当前租户无条件过滤,序号推进不跨租户。
  124. const string tenantFilter = "AND tenant_id = @TenantId2";
  125. var dayPars = new List<SugarParameter>
  126. {
  127. new("@RuleCode", rule.NbrType ?? ruleCode),
  128. new("@Domain", rule.Domain ?? domain),
  129. new("@Today", sequenceDate ?? DateTime.Today),
  130. new("@TenantId2", tenantId)
  131. };
  132. return (await _db.Ado.SqlQueryAsync<NbrDayInfoRow>(
  133. $"""
  134. SELECT
  135. RecID AS Id,
  136. NextValue
  137. FROM NbrDayInfo
  138. WHERE LOWER(TRIM(IFNULL(NbrType,''))) = LOWER(@RuleCode)
  139. AND IFNULL(TRIM(Domain),'') = @Domain
  140. {dateFilter}
  141. {tenantFilter}
  142. ORDER BY RecID
  143. LIMIT 1
  144. FOR UPDATE
  145. """,
  146. dayPars)).FirstOrDefault();
  147. }
  148. private async Task InsertDayInfoAsync(NbrControlRuleRow rule, DateTime? sequenceDate, int nextValue, string operatorNo, DateTime now)
  149. {
  150. await _db.Ado.ExecuteCommandAsync(
  151. """
  152. INSERT INTO NbrDayInfo
  153. (NbrType, Description, NbrPre1, NbrPre2, NbrPre3, Today,
  154. NextValue, IniValue, MinValue, `MaxValue`, ResetValue,
  155. AllowReset, AllowSkip, AllowManual, DateType, IsDateType,
  156. IsActive, IsConfirm, Domain, CreateUser, CreateTime, UpdateUser, UpdateTime, tenant_id)
  157. VALUES
  158. (@NbrType, @Description, @NbrPre1, @NbrPre2, @NbrPre3, @Today,
  159. @NextValue, @IniValue, @MinValue, @MaxValue, @ResetValue,
  160. @AllowReset, @AllowSkip, @AllowManual, @DateType, @IsDateType,
  161. @IsActive, @IsConfirm, @Domain, @UserNo, @Now, @UserNo, @Now, @TenantId)
  162. """,
  163. new SugarParameter("@NbrType", rule.NbrType ?? string.Empty),
  164. new SugarParameter("@Description", rule.Description ?? string.Empty),
  165. new SugarParameter("@NbrPre1", rule.NbrPre1 ?? string.Empty),
  166. new SugarParameter("@NbrPre2", rule.NbrPre2 ?? string.Empty),
  167. new SugarParameter("@NbrPre3", rule.NbrPre3 ?? string.Empty),
  168. new SugarParameter("@Today", sequenceDate ?? now.Date),
  169. new SugarParameter("@NextValue", nextValue),
  170. new SugarParameter("@IniValue", rule.IniValue),
  171. new SugarParameter("@MinValue", rule.MinValue),
  172. new SugarParameter("@MaxValue", rule.MaxValue),
  173. new SugarParameter("@ResetValue", rule.ResetValue),
  174. new SugarParameter("@AllowReset", rule.AllowReset),
  175. new SugarParameter("@AllowSkip", rule.AllowSkip),
  176. new SugarParameter("@AllowManual", rule.AllowManual),
  177. new SugarParameter("@DateType", rule.DateType ?? string.Empty),
  178. new SugarParameter("@IsDateType", rule.IsDateType),
  179. new SugarParameter("@IsActive", rule.IsActive ?? true),
  180. new SugarParameter("@IsConfirm", rule.IsConfirm ?? true),
  181. new SugarParameter("@Domain", rule.Domain ?? string.Empty),
  182. new SugarParameter("@UserNo", operatorNo ?? string.Empty),
  183. new SugarParameter("@Now", now),
  184. new SugarParameter("@TenantId", rule.TenantId));
  185. }
  186. private async Task UpdateDayInfoAsync(int dayInfoId, int nextValue, string operatorNo, DateTime now)
  187. {
  188. await _db.Ado.ExecuteCommandAsync(
  189. """
  190. UPDATE NbrDayInfo
  191. SET NextValue=@NextValue,
  192. UpdateUser=@UpdateUser,
  193. UpdateTime=@UpdateTime
  194. WHERE RecID=@Id
  195. """,
  196. new SugarParameter("@NextValue", nextValue),
  197. new SugarParameter("@UpdateUser", operatorNo ?? string.Empty),
  198. new SugarParameter("@UpdateTime", now),
  199. new SugarParameter("@Id", dayInfoId));
  200. }
  201. private static int ResolveFirstSerial(NbrControlRuleRow rule, NbrDayInfoRow? dayInfo)
  202. {
  203. if (dayInfo?.NextValue != null) return dayInfo.NextValue.Value + 1;
  204. if (rule.IniValue.HasValue) return rule.IniValue.Value;
  205. if (rule.MinValue.HasValue) return rule.MinValue.Value;
  206. return 1;
  207. }
  208. private sealed class NbrControlRuleRow
  209. {
  210. public int Id { get; set; }
  211. public string? NbrType { get; set; }
  212. public string? NbrPre1 { get; set; }
  213. public string? NbrPre2 { get; set; }
  214. public string? NbrPre3 { get; set; }
  215. public string? DateType { get; set; }
  216. public bool? IsDateType { get; set; }
  217. public string? Description { get; set; }
  218. public int? IniValue { get; set; }
  219. public int? MinValue { get; set; }
  220. public int? MaxValue { get; set; }
  221. public int? ResetValue { get; set; }
  222. public bool? AllowReset { get; set; }
  223. public bool? AllowSkip { get; set; }
  224. public bool? AllowManual { get; set; }
  225. public bool? IsActive { get; set; }
  226. public bool? IsConfirm { get; set; }
  227. public string? Domain { get; set; }
  228. public long? TenantId { get; set; }
  229. }
  230. private sealed class NbrDayInfoRow
  231. {
  232. public int Id { get; set; }
  233. public int? NextValue { get; set; }
  234. }
  235. }