StockQueryService.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. using Admin.NET.Plugin.AiDOP.Infrastructure;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  3. using Microsoft.Extensions.Options;
  4. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  5. /// <summary>
  6. /// S5 库存查询:经 IInventoryBalanceReader,优先 LIVE_165,回落 mdp_std_inventory。
  7. /// Domain 由 SourceDomainTenantResolver 反查,禁止回落 8010。
  8. /// </summary>
  9. [ApiDescriptionSettings(Order = 300, Description = "库存查询")]
  10. [Route("api/S5StockQuery")]
  11. [NonUnify]
  12. public class StockQueryService : IDynamicApiController, ITransient
  13. {
  14. private const long MainTenantId = 1300000000001L;
  15. private readonly ISqlSugarClient _db;
  16. private readonly UserManager _userManager;
  17. private readonly IInventoryBalanceReader _balanceReader;
  18. private readonly SourceDomainTenantResolver _domainTenant;
  19. private readonly AidopInventoryOptions _opt;
  20. public StockQueryService(
  21. ISqlSugarClient db,
  22. UserManager userManager,
  23. IInventoryBalanceReader balanceReader,
  24. SourceDomainTenantResolver domainTenant,
  25. IOptions<AidopInventoryOptions> opt)
  26. {
  27. _db = db;
  28. _userManager = userManager;
  29. _balanceReader = balanceReader;
  30. _domainTenant = domainTenant;
  31. _opt = opt.Value;
  32. }
  33. private long ResolveTenantOrThrow()
  34. {
  35. var tid = _userManager.TenantId;
  36. if (tid <= 0)
  37. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  38. if (_userManager.SuperAdmin && tid == MainTenantId)
  39. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  40. return tid;
  41. }
  42. private async Task<string> ResolveDomainOrThrowAsync(long tenantId, CancellationToken ct = default)
  43. {
  44. var sourceCode = string.IsNullOrWhiteSpace(_opt.SourceCode) ? "DOPDEMORQ_SQLSERVER" : _opt.SourceCode.Trim();
  45. try
  46. {
  47. return await _domainTenant.ResolveDomainAsync(sourceCode, tenantId, ct);
  48. }
  49. catch (Exception ex)
  50. {
  51. throw Oops.Oh($"无法解析当前租户 Domain 映射:{ex.Message}");
  52. }
  53. }
  54. private const string LocTypeExpr = "CASE WHEN t.typed='VMI' THEN 'VMI' WHEN t.typed='Supp' THEN 'O库' ELSE '' END";
  55. [DisplayName("库存查询列表")]
  56. [HttpGet("list")]
  57. public async Task<object> GetList([FromQuery] StockQueryListInput input, CancellationToken ct)
  58. {
  59. var tenantId = ResolveTenantOrThrow();
  60. var domain = await ResolveDomainOrThrowAsync(tenantId, ct);
  61. var page = await _balanceReader.QueryPageAsync(new InventoryBalanceQuery
  62. {
  63. TenantId = tenantId,
  64. Domain = domain,
  65. Type = input.Type,
  66. Location = input.Location,
  67. MaterialCode = input.MaterialCode,
  68. BatchNo = input.BatchNo,
  69. StockGt = input.StockGt,
  70. UnrestrictedGt = input.UnrestrictedGt,
  71. QcStockGt = input.QcStockGt,
  72. FrozenGt = input.FrozenGt,
  73. Page = input.Page,
  74. PageSize = input.PageSize
  75. }, ct);
  76. return new
  77. {
  78. total = page.Total,
  79. page = page.Page,
  80. pageSize = page.PageSize,
  81. list = page.List.Select(x => new StockQueryListRow
  82. {
  83. Type = x.Type,
  84. Location = x.Location,
  85. LocationName = x.LocationName,
  86. MaterialCode = x.MaterialCode,
  87. MaterialName = x.MaterialName,
  88. BatchNo = x.BatchNo,
  89. Shelf = x.Shelf,
  90. Version = x.Version,
  91. UnrestrictedStock = x.UnrestrictedStock,
  92. QcStock = x.QcStock,
  93. FrozenStock = x.FrozenStock,
  94. QtyOnHand = x.QtyOnHand,
  95. Unit = x.Unit,
  96. Supplier = x.Supplier
  97. }).ToList(),
  98. grandTotalQtyOnHand = page.GrandTotalQtyOnHand,
  99. grandUnrestricted = page.GrandUnrestricted,
  100. grandQc = page.GrandQc,
  101. grandFrozen = page.GrandFrozen,
  102. dataSource = page.DataSource,
  103. asOf = page.AsOf,
  104. isRealtime = page.IsRealtime,
  105. // true 仅表示「尝试实时源失败后降级」;无筛选时按策略本就走标准层,此处为 false
  106. fallbackFromLive = page.FallbackFromLive
  107. };
  108. }
  109. [DisplayName("库存查询类型选项")]
  110. [HttpGet("type-options")]
  111. public async Task<object> GetTypeOptions(CancellationToken ct)
  112. {
  113. var tenantId = ResolveTenantOrThrow();
  114. var domain = await ResolveDomainOrThrowAsync(tenantId, ct);
  115. var list = await _db.Ado.SqlQueryAsync<StockQueryTypeOption>(
  116. $"""
  117. SELECT DISTINCT {LocTypeExpr.Replace("t.typed", "typed")} AS Val
  118. FROM LocationMaster
  119. WHERE Domain = @Domain AND tenant_id = @TenantId AND {LocTypeExpr.Replace("t.typed", "typed")} <> ''
  120. ORDER BY Val
  121. """,
  122. new List<SugarParameter> { new("@Domain", domain), new("@TenantId", tenantId) });
  123. return list;
  124. }
  125. [DisplayName("库存查询库位选项")]
  126. [HttpGet("location-options")]
  127. public async Task<object> GetLocationOptions(CancellationToken ct)
  128. {
  129. var tenantId = ResolveTenantOrThrow();
  130. var domain = await ResolveDomainOrThrowAsync(tenantId, ct);
  131. // 与查询层白名单共用同一谓词(含 Typed<>'Supp'),
  132. // 避免出现「下拉能选到某库位、查询却永远 0 条」的口径不一致。
  133. var list = await TenantLocationScopeLoader.LoadOptionsAsync(_db, tenantId, domain, ct);
  134. return list.Select(x => new StockQueryLocationOption { Val = x.Val, Label = x.Label }).ToList();
  135. }
  136. }