InoutQueryService.cs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  3. /// <summary>
  4. /// S5 进出存查询 只读 list 服务。
  5. ///
  6. /// 数据源:aidopdev.InvTransHist(库存交易流水),忠实复刻证据 sql.txt:
  7. /// LEFT JOIN GeneralizedCodeMaster(TransType→名称)、LocationMaster(货位名)、ItemMaster(物料名/单位),
  8. /// 过滤 a.Domain=@Domain。进出存口径由 SQL 内定义(非脑补):
  9. /// 期初 = BeginBalance;入库 = QtyChange&gt;0?QtyChange:0;出库 = QtyChange&lt;0?abs(QtyChange):0;
  10. /// 期末(结存) = BeginBalance+QtyChange。逐笔交易流水,BeginBalance 按行存储。
  11. /// Domain 由 GeneralizedCodeMaster(SystemConfig/CompanyCode) 解析,默认 8010。
  12. /// 排序 a.RecID DESC(PK,近似交易时间倒序,兼顾 1M 行分页性能与稳定)。
  13. ///
  14. /// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转。
  15. /// 安全:InvTransHist / LocationMaster / GeneralizedCodeMaster 均含 tenant_id,主业务查询按当前租户强制过滤;
  16. /// 租户只来自认证后的可信 JWT(严格解析),忽略前端 tenantId,杜绝匿名/跨租户可见。
  17. /// </summary>
  18. [ApiDescriptionSettings(Order = 303, Description = "进出存查询")]
  19. [Route("api/S5InoutQuery")]
  20. [NonUnify]
  21. public class InoutQueryService : IDynamicApiController, ITransient
  22. {
  23. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  24. private const long MainTenantId = 1300000000001L;
  25. private readonly ISqlSugarClient _db;
  26. private readonly UserManager _userManager;
  27. public InoutQueryService(ISqlSugarClient db, UserManager userManager)
  28. {
  29. _db = db;
  30. _userManager = userManager;
  31. }
  32. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  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. /// <summary>解析公司域(GeneralizedCodeMaster SystemConfig/CompanyCode,默认 8010)。</summary>
  43. private async Task<string> ResolveDomainAsync()
  44. {
  45. var list = await _db.Ado.SqlQueryAsync<string>(
  46. "SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig' AND Val='CompanyCode' ORDER BY RecID LIMIT 1");
  47. var domain = list.FirstOrDefault();
  48. return string.IsNullOrWhiteSpace(domain) ? "8010" : domain;
  49. }
  50. /// <summary>
  51. /// 进出存查询列表(只读分页查询)。
  52. /// </summary>
  53. [DisplayName("进出存查询列表")]
  54. [HttpGet("list")]
  55. public async Task<object> GetList([FromQuery] InoutQueryListInput input)
  56. {
  57. var page = input.Page <= 0 ? 1 : input.Page;
  58. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  59. var offset = (page - 1) * pageSize;
  60. var tenantId = ResolveTenantOrThrow();
  61. var domain = await ResolveDomainAsync();
  62. // 强制租户过滤:InvTransHist 含 tenant_id(约百万行、当前全 797),按可信 JWT 租户过滤,count 与 list 同 tid。
  63. var where = new List<string> { "a.Domain = @Domain", "a.tenant_id = @TenantId" };
  64. var pars = new List<SugarParameter> { new("@Domain", domain), new("@TenantId", tenantId) };
  65. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  66. {
  67. where.Add("a.ItemNum LIKE @MaterialCode");
  68. pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
  69. }
  70. if (!string.IsNullOrWhiteSpace(input.Location))
  71. {
  72. where.Add("a.Loc = @Location");
  73. pars.Add(new SugarParameter("@Location", input.Location.Trim()));
  74. }
  75. if (!string.IsNullOrWhiteSpace(input.BatchNo))
  76. {
  77. where.Add("a.LotSerial LIKE @BatchNo");
  78. pars.Add(new SugarParameter("@BatchNo", $"%{input.BatchNo.Trim()}%"));
  79. }
  80. if (!string.IsNullOrWhiteSpace(input.WorkOrderNo))
  81. {
  82. where.Add("a.WorkOrd LIKE @WorkOrderNo");
  83. pars.Add(new SugarParameter("@WorkOrderNo", $"%{input.WorkOrderNo.Trim()}%"));
  84. }
  85. if (!string.IsNullOrWhiteSpace(input.TransType))
  86. {
  87. where.Add("a.TransType = @TransType");
  88. pars.Add(new SugarParameter("@TransType", input.TransType.Trim()));
  89. }
  90. if (!string.IsNullOrWhiteSpace(input.CreateTime))
  91. {
  92. where.Add("a.CreateTime >= @CreateFrom AND a.CreateTime < DATE_ADD(@CreateFrom, INTERVAL 1 DAY)");
  93. pars.Add(new SugarParameter("@CreateFrom", $"{input.CreateTime.Trim()} 00:00:00"));
  94. }
  95. if (!string.IsNullOrWhiteSpace(input.CreateUser))
  96. {
  97. where.Add("a.CreateUser LIKE @CreateUser");
  98. pars.Add(new SugarParameter("@CreateUser", $"%{input.CreateUser.Trim()}%"));
  99. }
  100. var whereSql = string.Join(" AND ", where);
  101. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM InvTransHist a WHERE {whereSql}", pars);
  102. var list = await _db.Ado.SqlQueryAsync<InoutQueryListRow>(
  103. $"""
  104. SELECT
  105. a.CreateTime AS TransTime,
  106. a.ItemNum AS MaterialCode,
  107. i.Descr AS MaterialName,
  108. a.Loc AS Location,
  109. IFNULL(l.descr,'') AS LocationName,
  110. a.LotSerial AS BatchNo,
  111. a.BeginBalance AS BeginQty,
  112. (CASE WHEN a.QtyChange > 0 THEN a.QtyChange ELSE 0 END) AS InQty,
  113. (CASE WHEN a.QtyChange < 0 THEN ABS(a.QtyChange) ELSE 0 END) AS OutQty,
  114. (a.BeginBalance + a.QtyChange) AS EndQty,
  115. i.UM AS Unit,
  116. COALESCE(b.Comments, a.TransType) AS TransType,
  117. a.WorkOrd AS WorkOrderNo,
  118. a.ShipperNum AS DeliveryNo,
  119. a.ShipType AS DeliveryType,
  120. a.Remark AS Remark,
  121. a.CreateUser AS CreateUser
  122. FROM InvTransHist a
  123. LEFT JOIN GeneralizedCodeMaster b ON a.Domain = b.Domain AND a.TransType = b.Val AND b.FldName = 'TransType'
  124. LEFT JOIN LocationMaster l ON a.Domain = l.Domain AND a.Loc = l.location
  125. LEFT JOIN ItemMaster i ON a.Domain = i.Domain AND a.ItemNum = i.ItemNum
  126. WHERE {whereSql}
  127. ORDER BY a.RecID DESC
  128. LIMIT {pageSize} OFFSET {offset}
  129. """,
  130. pars);
  131. return new { total, page, pageSize, list };
  132. }
  133. /// <summary>
  134. /// 货位下拉选项(LocationMaster.Location + Descr,只读)。
  135. /// </summary>
  136. [DisplayName("进出存查询货位选项")]
  137. [HttpGet("loc-options")]
  138. public async Task<object> GetLocationOptions()
  139. {
  140. var tenantId = ResolveTenantOrThrow();
  141. var domain = await ResolveDomainAsync();
  142. // LocationMaster 含 tenant_id(797:80 / 824:34),强制按可信租户过滤。
  143. var list = await _db.Ado.SqlQueryAsync<InoutQueryLocationOption>(
  144. """
  145. SELECT location AS Val, descr AS Label
  146. FROM LocationMaster
  147. WHERE Domain = @Domain AND tenant_id = @TenantId AND IFNULL(location,'') <> ''
  148. ORDER BY location
  149. """,
  150. new List<SugarParameter> { new("@Domain", domain), new("@TenantId", tenantId) });
  151. return list;
  152. }
  153. /// <summary>
  154. /// 交易类型下拉选项(GeneralizedCodeMaster TransType:代码 + 名称,只读)。
  155. /// </summary>
  156. [DisplayName("进出存查询交易类型选项")]
  157. [HttpGet("transtype-options")]
  158. public async Task<object> GetTransTypeOptions()
  159. {
  160. var tenantId = ResolveTenantOrThrow();
  161. var domain = await ResolveDomainAsync();
  162. // GeneralizedCodeMaster 含 tenant_id(797/824),强制按可信租户过滤。
  163. var list = await _db.Ado.SqlQueryAsync<InoutQueryTransTypeOption>(
  164. """
  165. SELECT Val AS Val, Comments AS Label
  166. FROM GeneralizedCodeMaster
  167. WHERE FldName = 'TransType' AND Domain = @Domain AND tenant_id = @TenantId AND IFNULL(Val,'') <> ''
  168. ORDER BY Val
  169. """,
  170. new List<SugarParameter> { new("@Domain", domain), new("@TenantId", tenantId) });
  171. return list;
  172. }
  173. }