PendingInspectionService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. /// Domain 由 GeneralizedCodeMaster(SystemConfig/CompanyCode) 解析,默认 8010。
  11. ///
  12. /// 本服务仅 SELECT:无新增/编辑/删除/库存事务/状态流转;MissedPrint 无 tenant_id,不做租户过滤。
  13. /// </summary>
  14. [ApiDescriptionSettings(Order = 301, Description = "暂收在检列表")]
  15. [Route("api/S5PendingInspection")]
  16. [NonUnify]
  17. public class PendingInspectionService : IDynamicApiController, ITransient
  18. {
  19. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  20. private const long MainTenantId = 1300000000001L;
  21. /// <summary>贴源源码:与 S5 其余库存页保持一致,供 Domain 解析使用。</summary>
  22. private const string SourceCode = "DOPDEMORQ_SQLSERVER";
  23. private readonly ISqlSugarClient _db;
  24. private readonly UserManager _userManager;
  25. private readonly SourceDomainTenantResolver _domainTenant;
  26. public PendingInspectionService(
  27. ISqlSugarClient db, UserManager userManager, SourceDomainTenantResolver domainTenant)
  28. {
  29. _db = db;
  30. _userManager = userManager;
  31. _domainTenant = domainTenant;
  32. }
  33. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  34. private long ResolveTenantOrThrow()
  35. {
  36. var tid = _userManager.TenantId;
  37. if (tid <= 0)
  38. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  39. if (_userManager.SuperAdmin && tid == MainTenantId)
  40. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  41. return tid;
  42. }
  43. /// <summary>
  44. /// 按当前租户解析公司域,统一走 <see cref="SourceDomainTenantResolver"/>。
  45. /// <para>
  46. /// 原实现是 <c>SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig'
  47. /// AND Val='CompanyCode' ORDER BY RecID LIMIT 1</c> —— **无 tenant 条件**,取的是全库第一条,
  48. /// 与 resolver 修复前的缺陷同源:不同租户会被解析到同一个(别人的)Domain,
  49. /// 且解析不到时硬回落 "8010"。本项目的 Domain 解析只允许有一个正式实现。
  50. /// </para>
  51. /// </summary>
  52. private async Task<string> ResolveDomainOrThrowAsync(long tenantId, CancellationToken ct = default)
  53. {
  54. try
  55. {
  56. return await _domainTenant.ResolveDomainAsync(SourceCode, tenantId, ct);
  57. }
  58. catch (Exception ex)
  59. {
  60. // fail closed:解析不出租户 Domain 就不查,绝不回落固定值
  61. throw Oops.Oh($"无法解析当前租户 Domain 映射:{ex.Message}");
  62. }
  63. }
  64. /// <summary>
  65. /// 暂收在检列表(只读分页查询)。
  66. /// </summary>
  67. [DisplayName("暂收在检列表")]
  68. [HttpGet("list")]
  69. public async Task<object> GetList([FromQuery] PendingInspectionListInput input)
  70. {
  71. var page = input.Page <= 0 ? 1 : input.Page;
  72. var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
  73. var offset = (page - 1) * pageSize;
  74. // 租户先解析,Domain 由租户决定(不再全局取第一条)
  75. var listTenantId = ResolveTenantOrThrow();
  76. var domain = await ResolveDomainOrThrowAsync(listTenantId);
  77. var where = new List<string>
  78. {
  79. "m.Domain = @Domain",
  80. "m.Status = 'I'",
  81. "m.Qty > 0",
  82. "IFNULL(m.Location,'') = ''",
  83. };
  84. // 同库分租户:主表 MissedPrint 无 tenant_id(只能靠 Domain 公司域隔离,无法做租户行级过滤);
  85. // LEFT JOIN 的 qms 申请/明细表已补 tenant_id,租户过滤置于 JOIN ON(避免退化为 INNER JOIN 丢失 MissedPrint 锚行)。
  86. // 租户改用严格可信解析(仅认证后 JWT,忽略前端 tenantId);配合类级已去 [AllowAnonymous],杜绝匿名跨租户读。
  87. // 残留边界:MissedPrint 锚行本身仍按 Domain(非租户)可见,此为旧系统 pr_WMS_GetMissedTempQty 无租户列的既有约束。
  88. var tenantId = listTenantId;
  89. var pars = new List<SugarParameter> { new("@Domain", domain), new("@TenantId", tenantId) };
  90. if (!string.IsNullOrWhiteSpace(input.DemandNo))
  91. {
  92. where.Add("m.PurOrd LIKE @DemandNo");
  93. pars.Add(new SugarParameter("@DemandNo", $"%{input.DemandNo.Trim()}%"));
  94. }
  95. if (!string.IsNullOrWhiteSpace(input.Shelf))
  96. {
  97. where.Add("m.Shelf LIKE @Shelf");
  98. pars.Add(new SugarParameter("@Shelf", $"%{input.Shelf.Trim()}%"));
  99. }
  100. if (!string.IsNullOrWhiteSpace(input.BatchNo))
  101. {
  102. where.Add("m.LotSerial LIKE @BatchNo");
  103. pars.Add(new SugarParameter("@BatchNo", $"%{input.BatchNo.Trim()}%"));
  104. }
  105. if (!string.IsNullOrWhiteSpace(input.MaterialCode))
  106. {
  107. where.Add("m.ItemNum LIKE @MaterialCode");
  108. pars.Add(new SugarParameter("@MaterialCode", $"%{input.MaterialCode.Trim()}%"));
  109. }
  110. if (!string.IsNullOrWhiteSpace(input.InspectStatus))
  111. {
  112. where.Add("d.FINSPECTSTATUS = @InspectStatus");
  113. pars.Add(new SugarParameter("@InspectStatus", input.InspectStatus.Trim()));
  114. }
  115. if (!string.IsNullOrWhiteSpace(input.Supplier))
  116. {
  117. where.Add("m.Supply = @Supplier");
  118. pars.Add(new SugarParameter("@Supplier", input.Supplier.Trim()));
  119. }
  120. var whereSql = string.Join(" AND ", where);
  121. // 复刻存储过程 @tbLocDet 聚合层
  122. var groupedInner =
  123. $"""
  124. SELECT m.Supply, m.PurOrd, m.PurLine, m.ItemNum, m.ShipperNbr, m.LotSerial,
  125. MAX(m.Shelf) AS Shelf, SUM(m.Qty) AS Qty, MAX(m.CheckNo) AS CheckNo,
  126. MAX(IFNULL(d.FINSPECTSTATUS,'')) AS FINSPECTSTATUS
  127. FROM MissedPrint m
  128. LEFT JOIN qms_qcp_inspecapplyn q ON m.CheckNo = q.FBILLNO AND q.tenant_id = @TenantId
  129. LEFT JOIN qms_qcp_insappnentry d ON q.id = d.glid AND d.tenant_id = @TenantId
  130. WHERE {whereSql}
  131. GROUP BY m.Supply, m.PurOrd, m.PurLine, m.ShipperNbr, m.ItemNum, m.LotSerial
  132. """;
  133. var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM ({groupedInner}) t", pars);
  134. var grandList = await _db.Ado.SqlQueryAsync<decimal>($"SELECT IFNULL(SUM(t.Qty),0) FROM ({groupedInner}) t", pars);
  135. var grandTotalQty = grandList.FirstOrDefault();
  136. var list = await _db.Ado.SqlQueryAsync<PendingInspectionListRow>(
  137. $"""
  138. SELECT
  139. t.PurOrd AS DemandNo,
  140. t.PurLine AS DemandLineNo,
  141. t.ItemNum AS MaterialCode,
  142. i.Descr AS MaterialName,
  143. t.LotSerial AS BatchNo,
  144. t.Qty AS Qty,
  145. i.UM AS Unit,
  146. t.Shelf AS PendingShelf,
  147. t.ShipperNbr AS DeliveryNo,
  148. t.CheckNo AS InspectNo,
  149. CONCAT(t.FINSPECTSTATUS,
  150. CASE WHEN (SELECT MAX(ib.clfs) FROM qms_qcp_inspbill ib WHERE ib.lydjbh = t.CheckNo AND ib.tenant_id = @TenantId) = '3'
  151. THEN '-挑选使用' ELSE '' END) AS InspectStatus,
  152. t.Supply AS Supplier,
  153. s.SortName AS SupplierShortName
  154. FROM ({groupedInner}) t
  155. LEFT JOIN ItemMaster i ON i.Domain = @Domain AND t.ItemNum = i.ItemNum
  156. LEFT JOIN SuppMaster s ON s.Domain = @Domain AND t.Supply = s.Supp
  157. ORDER BY t.Supply, t.PurOrd, t.PurLine
  158. LIMIT {pageSize} OFFSET {offset}
  159. """,
  160. pars);
  161. return new { total, page, pageSize, list, grandTotalQty };
  162. }
  163. /// <summary>
  164. /// 检验状态下拉选项(qms_qcp_insappnentry.FINSPECTSTATUS distinct,只读)。
  165. /// </summary>
  166. [DisplayName("暂收在检检验状态选项")]
  167. [HttpGet("status-options")]
  168. public async Task<object> GetStatusOptions()
  169. {
  170. // 同库分租户:qms_qcp_insappnentry 已补 tenant_id,选项按严格可信租户(仅认证后 JWT,忽略前端 tenantId)收敛。
  171. var tenantId = ResolveTenantOrThrow();
  172. var list = await _db.Ado.SqlQueryAsync<PendingInspectionStatusOption>(
  173. """
  174. SELECT DISTINCT FINSPECTSTATUS AS Val
  175. FROM qms_qcp_insappnentry
  176. WHERE IFNULL(FINSPECTSTATUS,'') <> '' AND tenant_id = @TenantId
  177. ORDER BY FINSPECTSTATUS
  178. """,
  179. new List<SugarParameter> { new("@TenantId", tenantId) });
  180. return list;
  181. }
  182. /// <summary>
  183. /// 供应商下拉选项(暂收在检集合内 distinct Supply + SuppMaster.SortName,只读)。
  184. /// </summary>
  185. [DisplayName("暂收在检供应商选项")]
  186. [HttpGet("supplier-options")]
  187. public async Task<object> GetSupplierOptions()
  188. {
  189. // 残留边界(注明):本选项来自 MissedPrint(无 tenant_id)+ SuppMaster(无 tenant_id),无 tenant-safe 父键可挂载租户过滤,
  190. // 只能按 Domain 公司域收敛,无法做租户行级过滤(既有约束)。类级已去 [AllowAnonymous] → 需认证后方可访问,杜绝匿名读。
  191. var domain = await ResolveDomainOrThrowAsync(ResolveTenantOrThrow());
  192. var list = await _db.Ado.SqlQueryAsync<PendingInspectionSupplierOption>(
  193. """
  194. SELECT DISTINCT m.Supply AS Val, s.SortName AS Label
  195. FROM MissedPrint m
  196. LEFT JOIN SuppMaster s ON s.Domain = @Domain AND m.Supply = s.Supp
  197. WHERE m.Domain = @Domain AND m.Status = 'I' AND m.Qty > 0 AND IFNULL(m.Location,'') = ''
  198. AND IFNULL(m.Supply,'') <> ''
  199. ORDER BY m.Supply
  200. """,
  201. new List<SugarParameter> { new("@Domain", domain) });
  202. return list;
  203. }
  204. }