PendingInspectionService.cs 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  3. /// <summary>
  4. /// S5 暂收在检列表 只读 list 服务。
  5. ///
  6. /// 数据源:aidopdev.MissedPrint(Status='I' 暂收待检、Qty>0、Location 空),忠实复刻旧系统存储过程
  7. /// pr_WMS_GetMissedTempQty:LEFT JOIN qms_qcp_inspecapplyn/qms_qcp_insappnentry 取检验状态,按
  8. /// Supply/PurOrd/PurLine/ShipperNbr/ItemNum/LotSerial 聚合(MAX Shelf/SUM Qty/MAX CheckNo/MAX FINSPECTSTATUS),
  9. /// 再 LEFT JOIN ItemMaster(描述/单位)、SuppMaster(简称),clfs='3' 追加“-挑选使用”。
  10. /// 租户边界取 MissedPrint.tenant_id(正式租户列,已由生成标签写入 + 1.0.442 回填补齐)。
  11. /// Domain **不再**参与 MissedPrint 及其主数据关联的过滤,理由见 GetList 内注释。
  12. ///
  13. /// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转;MissedPrint 无 tenant_id,不做租户过滤。
  14. /// </summary>
  15. [ApiDescriptionSettings(Order = 301, Description = "暂收在检列表")]
  16. [Route("api/S5PendingInspection")]
  17. [NonUnify]
  18. public class PendingInspectionService : IDynamicApiController, ITransient
  19. {
  20. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  21. private const long MainTenantId = 1300000000001L;
  22. private readonly ISqlSugarClient _db;
  23. private readonly UserManager _userManager;
  24. public PendingInspectionService(ISqlSugarClient db, UserManager userManager)
  25. {
  26. _db = db;
  27. _userManager = userManager;
  28. }
  29. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  30. private long ResolveTenantOrThrow()
  31. {
  32. var tid = _userManager.TenantId;
  33. if (tid <= 0)
  34. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  35. if (_userManager.SuperAdmin && tid == MainTenantId)
  36. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  37. return tid;
  38. }
  39. /// <summary>
  40. /// 暂收在检列表(只读分页查询)。
  41. /// </summary>
  42. [DisplayName("暂收在检列表")]
  43. [HttpGet("list")]
  44. public async Task<object> GetList([FromQuery] PendingInspectionListInput input)
  45. {
  46. var page = input.Page <= 0 ? 1 : input.Page;
  47. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  48. var offset = (page - 1) * pageSize;
  49. var tenantId = ResolveTenantOrThrow();
  50. // 租户边界 = MissedPrint.tenant_id。
  51. //
  52. // 这里**刻意不再带 m.Domain = @Domain**,依据(2026-09-02 实测 aidopdev):
  53. // ① Domain 不是租户边界 —— 797 与 UAT 的 Domain 解析结果同为 '8010';
  54. // ② MissedPrint 里每个租户只有单一 Domain 值(797 全为 '8010'、UAT 全为空串),
  55. // 所以叠加 Domain 对**租户内**没有任何过滤作用,只可能整租户命中或整租户清零;
  56. // ③ 而 UAT 恰好就是被整租户清零的那一种:SourceDomainTenantResolver 经 LocationMaster
  57. // 回落给出 '8010',UAT 自己的 48 行 Domain 却是空串 —— 旧写法下 UAT 的候选宇宙
  58. // 100% 由 797 的 104 行构成,自己的数据一行都进不来,方向是反的。
  59. // 结论:Domain 在本查询里既无过滤价值又会误杀,由 tenant_id 完全取代。
  60. var where = new List<string>
  61. {
  62. "m.tenant_id = @TenantId",
  63. "m.Status = 'I'",
  64. "m.Qty > 0",
  65. "IFNULL(m.Location,'') = ''",
  66. };
  67. // qms 申请/明细的租户过滤置于 JOIN ON(避免 LEFT JOIN 退化为 INNER JOIN 丢失 MissedPrint 锚行)
  68. var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
  69. if (!string.IsNullOrWhiteSpace(input.DemandNo))
  70. {
  71. where.Add("m.PurOrd LIKE @DemandNo");
  72. pars.Add(new SugarParameter("@DemandNo", $"%{input.DemandNo.Trim()}%"));
  73. }
  74. if (!string.IsNullOrWhiteSpace(input.Shelf))
  75. {
  76. where.Add("m.Shelf LIKE @Shelf");
  77. pars.Add(new SugarParameter("@Shelf", $"%{input.Shelf.Trim()}%"));
  78. }
  79. if (!string.IsNullOrWhiteSpace(input.BatchNo))
  80. {
  81. where.Add("m.LotSerial LIKE @BatchNo");
  82. pars.Add(new SugarParameter("@BatchNo", $"%{input.BatchNo.Trim()}%"));
  83. }
  84. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  85. {
  86. where.Add("m.ItemNum LIKE @MaterialCode");
  87. pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
  88. }
  89. if (!string.IsNullOrWhiteSpace(input.InspectStatus))
  90. {
  91. where.Add("d.FINSPECTSTATUS = @InspectStatus");
  92. pars.Add(new SugarParameter("@InspectStatus", input.InspectStatus.Trim()));
  93. }
  94. if (!string.IsNullOrWhiteSpace(input.Supplier))
  95. {
  96. where.Add("m.Supply = @Supplier");
  97. pars.Add(new SugarParameter("@Supplier", input.Supplier.Trim()));
  98. }
  99. var whereSql = string.Join(" AND ", where);
  100. // 复刻存储过程 @tbLocDet 聚合层
  101. var groupedInner =
  102. $"""
  103. SELECT m.Supply, m.PurOrd, m.PurLine, m.ItemNum, m.ShipperNbr, m.LotSerial,
  104. MAX(m.Shelf) AS Shelf, SUM(m.Qty) AS Qty, MAX(m.CheckNo) AS CheckNo,
  105. MAX(IFNULL(d.FINSPECTSTATUS,'')) AS FINSPECTSTATUS
  106. FROM MissedPrint m
  107. LEFT JOIN qms_qcp_inspecapplyn q ON m.CheckNo = q.FBILLNO AND q.tenant_id = @TenantId
  108. LEFT JOIN qms_qcp_insappnentry d ON q.id = d.glid AND d.tenant_id = @TenantId
  109. WHERE {whereSql}
  110. GROUP BY m.Supply, m.PurOrd, m.PurLine, m.ShipperNbr, m.ItemNum, m.LotSerial
  111. """;
  112. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM ({groupedInner}) t", pars);
  113. var grandList = await _db.Ado.SqlQueryAsync<decimal>($"SELECT IFNULL(SUM(t.Qty),0) FROM ({groupedInner}) t", pars);
  114. var grandTotalQty = grandList.FirstOrDefault();
  115. var list = await _db.Ado.SqlQueryAsync<PendingInspectionListRow>(
  116. $"""
  117. SELECT
  118. t.PurOrd AS DemandNo,
  119. t.PurLine AS DemandLineNo,
  120. t.ItemNum AS MaterialCode,
  121. i.Descr AS MaterialName,
  122. t.LotSerial AS BatchNo,
  123. t.Qty AS Qty,
  124. i.UM AS Unit,
  125. t.Shelf AS PendingShelf,
  126. t.ShipperNbr AS DeliveryNo,
  127. t.CheckNo AS InspectNo,
  128. CONCAT(t.FINSPECTSTATUS,
  129. CASE WHEN (SELECT MAX(ib.clfs) FROM qms_qcp_inspbill ib WHERE ib.lydjbh = t.CheckNo AND ib.tenant_id = @TenantId) = '3'
  130. THEN '-挑选使用' ELSE '' END) AS InspectStatus,
  131. t.Supply AS Supplier,
  132. s.SortName AS SupplierShortName
  133. FROM ({groupedInner}) t
  134. -- 主数据按租户关联,不按 Domain:(tenant_id, ItemNum) 与 (tenant_id, Supp) 都唯一,
  135. -- 而 (Domain, ItemNum) 有 391 组重复、(Domain, Supp) 有 14 组重复,
  136. -- Domain-only 关联会一对多放大,使分页取回行数与 COUNT 出来的 total 对不上。
  137. LEFT JOIN ItemMaster i ON i.tenant_id = @TenantId AND t.ItemNum = i.ItemNum
  138. LEFT JOIN SuppMaster s ON s.tenant_id = @TenantId AND t.Supply = s.Supp
  139. ORDER BY t.Supply, t.PurOrd, t.PurLine
  140. LIMIT {pageSize} OFFSET {offset}
  141. """,
  142. pars);
  143. return new { total, page, pageSize, list, grandTotalQty };
  144. }
  145. /// <summary>
  146. /// 检验状态下拉选项(qms_qcp_insappnentry.FINSPECTSTATUS distinct,只读)。
  147. /// </summary>
  148. [DisplayName("暂收在检检验状态选项")]
  149. [HttpGet("status-options")]
  150. public async Task<object> GetStatusOptions()
  151. {
  152. // 同库分租户:qms_qcp_insappnentry 已补 tenant_id,选项按严格可信租户(仅认证后 JWT,忽略前端 tenantId)收敛。
  153. var tenantId = ResolveTenantOrThrow();
  154. var list = await _db.Ado.SqlQueryAsync<PendingInspectionStatusOption>(
  155. """
  156. SELECT DISTINCT FINSPECTSTATUS AS Val
  157. FROM qms_qcp_insappnentry
  158. WHERE IFNULL(FINSPECTSTATUS,'') <> '' AND tenant_id = @TenantId
  159. ORDER BY FINSPECTSTATUS
  160. """,
  161. new List<SugarParameter> { new("@TenantId", tenantId) });
  162. return list;
  163. }
  164. /// <summary>
  165. /// 供应商下拉选项(暂收在检集合内 distinct Supply + SuppMaster.SortName,只读)。
  166. /// </summary>
  167. [DisplayName("暂收在检供应商选项")]
  168. [HttpGet("supplier-options")]
  169. public async Task<object> GetSupplierOptions()
  170. {
  171. // 与主查询同口径:租户边界取 tenant_id,MissedPrint 与 SuppMaster 都按租户收敛,
  172. // 下拉集合不会再出现别的租户的供应商(Supply 编码跨租户重名不影响,集合本身已按租户切分)。
  173. var tenantId = ResolveTenantOrThrow();
  174. var list = await _db.Ado.SqlQueryAsync<PendingInspectionSupplierOption>(
  175. """
  176. SELECT DISTINCT m.Supply AS Val, s.SortName AS Label
  177. FROM MissedPrint m
  178. LEFT JOIN SuppMaster s ON s.tenant_id = @TenantId AND m.Supply = s.Supp
  179. WHERE m.tenant_id = @TenantId AND m.Status = 'I' AND m.Qty > 0 AND IFNULL(m.Location,'') = ''
  180. AND IFNULL(m.Supply,'') <> ''
  181. ORDER BY m.Supply
  182. """,
  183. new List<SugarParameter> { new("@TenantId", tenantId) });
  184. return list;
  185. }
  186. }